feat(local_send): add local send support

This commit is contained in:
ZhuJHua
2024-11-11 03:51:17 +08:00
parent bef23b8426
commit 52b6ad0462
42 changed files with 988 additions and 359 deletions

View File

@@ -1,9 +1,7 @@
package cn.yooss.mood_diary
import android.os.Build
import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import androidx.annotation.RequiresApi
import androidx.core.view.WindowCompat
import com.github.gzuliyujiang.oaid.DeviceID
import io.flutter.embedding.android.FlutterFragmentActivity
import io.flutter.embedding.engine.FlutterEngine
@@ -11,20 +9,13 @@ import io.flutter.plugin.common.MethodChannel
class MainActivity : FlutterFragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(
flutterEngine.dartExecutor.binaryMessenger, "view_channel"
).setMethodCallHandler { call, result ->
if (call.method == "setSystemUIVisibility") {
setSystemUIVisibility()
result.success(null)
} else {
result.notImplemented()
}
}
MethodChannel(
flutterEngine.dartExecutor.binaryMessenger, "oaid_channel"
).setMethodCallHandler { call, result ->
@@ -47,9 +38,4 @@ class MainActivity : FlutterFragmentActivity() {
}
}
private fun setSystemUIVisibility() {
WindowCompat.setDecorFitsSystemWindows(window, false)
enableEdgeToEdge()
}
}

View File

@@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '12.0'
platform :ios, '18.2'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'

View File

@@ -85,6 +85,8 @@ PODS:
- Flutter
- media_kit_video (0.0.1):
- Flutter
- network_info_plus (0.0.1):
- Flutter
- ObjectBox (4.0.1)
- objectbox_flutter_libs (0.0.1):
- Flutter
@@ -100,7 +102,6 @@ PODS:
- Flutter
- record_darwin (1.0.0):
- Flutter
- FlutterMacOS
- screen_brightness_ios (0.1.0):
- Flutter
- SDWebImage (5.19.7):
@@ -166,6 +167,7 @@ DEPENDENCIES:
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
- media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`)
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
- network_info_plus (from `.symlinks/plugins/network_info_plus/ios`)
- objectbox_flutter_libs (from `.symlinks/plugins/objectbox_flutter_libs/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
@@ -230,6 +232,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/media_kit_native_event_loop/ios"
media_kit_video:
:path: ".symlinks/plugins/media_kit_video/ios"
network_info_plus:
:path: ".symlinks/plugins/network_info_plus/ios"
objectbox_flutter_libs:
:path: ".symlinks/plugins/objectbox_flutter_libs/ios"
package_info_plus:
@@ -281,13 +285,14 @@ SPEC CHECKSUMS:
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
network_info_plus: 6613d9d7cdeb0e6f366ed4dbe4b3c51c52d567a9
ObjectBox: 0bc4bb75eea85f6af06b369148b334c2056bbc29
objectbox_flutter_libs: 2ce0da386c780878687c736b528ceaf371573efb
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
quill_native_bridge_ios: 277bdf5bf0fa5d7a12999556b415a5c63dd76ec4
record_darwin: df0a677188e5fed18472550298e675f19ddaffbe
record_darwin: 3b1a8e7d5c0cbf45ad6165b4d83a6ca643d929c3
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
SDWebImage: 8a6b7b160b4d710e2a22b6900e25301075c34cb3
SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380
@@ -303,6 +308,6 @@ SPEC CHECKSUMS:
volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9
wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1
PODFILE CHECKSUM: 83893f9a2d98085e99381d8f1dc69dcf7bd5b437
PODFILE CHECKSUM: 9752b5340b4d3f9618318fcd530d907790e2a9f5
COCOAPODS: 1.15.2

View File

@@ -8,6 +8,7 @@
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
306627225710E691EF0E0A35 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2A1A8341695BDC0741989253 /* Pods_Runner.framework */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
@@ -15,7 +16,6 @@
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
B89FBBDC71186C5741E699DA /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2A1A8341695BDC0741989253 /* Pods_Runner.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -68,11 +68,11 @@
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
523FF08A9B798D86BA8B9DD9 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
B89FBBDC71186C5741E699DA /* Pods_Runner.framework in Frameworks */,
306627225710E691EF0E0A35 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -193,12 +193,12 @@
3190980419AFCAD7AC11C68D /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
DDBAC86E6B3F372170C284A0 /* [CP] Embed Pods Frameworks */,
B482CF705AC6985E445540F2 /* [CP] Copy Pods Resources */,
523FF08A9B798D86BA8B9DD9 /* Frameworks */,
);
buildRules = (
);
@@ -510,6 +510,7 @@
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 3XA29H789G;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = cn.yooss.moodDiary.RunnerTests;
@@ -528,6 +529,7 @@
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 3XA29H789G;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = cn.yooss.moodDiary.RunnerTests;
@@ -544,6 +546,7 @@
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 3XA29H789G;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = cn.yooss.moodDiary.RunnerTests;

View File

@@ -53,7 +53,8 @@ class Api {
Future<List<String>?> updatePosition() async {
Position? position;
if (await Utils().permissionUtil.checkPermission(Permission.location)) {
if (await Utils().permissionUtil.checkPermission(Permission.location) &&
await Geolocator.isLocationServiceEnabled()) {
position = await Geolocator.getLastKnownPosition(forceAndroidLocationManager: true);
position ??= await Geolocator.getCurrentPosition(locationSettings: AndroidSettings(forceLocationManager: true));
}

View File

@@ -113,6 +113,48 @@ class Diary {
}
Diary();
// 将 Diary 对象转换为 JSON
Map<String, dynamic> toJson() {
return {
'id': id,
'categoryId': categoryId,
'title': title,
'content': content,
'contentText': contentText,
'time': time.toIso8601String(),
'show': show,
'mood': mood,
'weather': weather,
'imageName': imageName,
'audioName': audioName,
'videoName': videoName,
'tags': tags,
'position': position,
'imageColor': imageColor,
'aspect': aspect,
};
}
factory Diary.fromJson(Map<String, dynamic> json) {
return Diary()
..id = json['id'] as String
..categoryId = json['categoryId'] as String?
..title = json['title'] as String
..content = json['content'] as String
..contentText = json['contentText'] as String
..time = DateTime.parse(json['time'] as String)
..show = json['show'] as bool
..mood = (json['mood'] as num).toDouble()
..weather = List<String>.from(json['weather'] as List)
..imageName = List<String>.from(json['imageName'] as List)
..audioName = List<String>.from(json['audioName'] as List)
..videoName = List<String>.from(json['videoName'] as List)
..tags = List<String>.from(json['tags'] as List)
..position = List<String>.from(json['position'] as List)
..imageColor = json['imageColor'] as int?
..aspect = (json['aspect'] as num?)?.toDouble();
}
}
int fastHash(String string) {

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
import 'package:mood_diary/common/values/icons.dart';
import 'package:mood_diary/components/mood_icon/mood_icon_view.dart';
@@ -28,7 +29,7 @@ class DashboardComponent extends StatelessWidget {
Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Icon(
FaIcon(
icon,
color: colorScheme.secondary,
size: 32,

View File

@@ -17,15 +17,13 @@ class DiaryTabViewComponent extends StatelessWidget {
@override
Widget build(BuildContext context) {
final logicTag = categoryId ?? 'default';
final logic =
Get.put(DiaryTabViewLogic(categoryId: categoryId), tag: logicTag);
final logic = Get.put(DiaryTabViewLogic(categoryId: categoryId), tag: logicTag);
final state = Bind.find<DiaryTabViewLogic>(tag: logicTag).state;
final i18n = AppLocalizations.of(context)!;
Widget buildGrid() {
return SliverWaterfallFlow(
gridDelegate: const SliverWaterfallFlowDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 250),
gridDelegate: const SliverWaterfallFlowDelegateWithMaxCrossAxisExtent(maxCrossAxisExtent: 250),
delegate: SliverChildBuilderDelegate(
(context, index) {
return LargeDiaryCardComponent(
@@ -68,29 +66,28 @@ class DiaryTabViewComponent extends StatelessWidget {
builder: (_) {
return buildPlaceHolder();
}),
CustomScrollView(
cacheExtent: 2000,
slivers: [
SliverOverlapInjector(
handle:
NestedScrollView.sliverOverlapAbsorberHandleFor(context)),
SliverPadding(
padding: const EdgeInsets.all(4.0),
sliver: GetBuilder<DiaryTabViewLogic>(
id: 'TabView',
tag: logicTag,
builder: (_) {
return SliverAnimatedSwitcher(
GetBuilder<DiaryTabViewLogic>(
id: 'TabView',
tag: logicTag,
builder: (_) {
return CustomScrollView(
primary: true,
cacheExtent: 2000,
slivers: [
SliverOverlapInjector(handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context)),
SliverPadding(
padding: const EdgeInsets.all(4.0),
sliver: SliverAnimatedSwitcher(
duration: const Duration(milliseconds: 400),
child: switch (logic.diaryLogic.state.viewModeType) {
ViewModeType.list => buildList(),
ViewModeType.grid => buildGrid(),
},
);
}),
),
],
),
),
),
],
);
}),
],
);
}

View File

@@ -0,0 +1,172 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:dio/dio.dart' as dio;
import 'package:get/get.dart';
import 'package:mood_diary/common/models/isar/diary.dart';
import 'package:mood_diary/utils/utils.dart';
import 'local_send_client_state.dart';
class LocalSendClientLogic extends GetxController {
final LocalSendClientState state = LocalSendClientState();
late RawDatagramSocket socket;
Timer? timer;
@override
void onReady() async {
super.onReady();
socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 0);
socket.broadcastEnabled = true;
await startFindServer();
}
@override
void onClose() {
socket.close();
timer?.cancel();
super.onClose();
}
void _sendBroadcast() {
const message = 'Looking for server';
socket.send(message.codeUnits, InternetAddress('255.255.255.255'), state.scanPort);
Utils().logUtil.printInfo('Broadcast sent');
}
// 尝试在 30 秒内找到服务器
Future<bool> startFindServer() async {
state.isFindingServer = true;
update();
final found = await _findServer(timeout: const Duration(seconds: 30));
if (found) {
Utils().noticeUtil.showToast('找到服务器');
} else {
state.isFindingServer = false;
update();
}
return found;
}
// 重新开始查找服务器
Future<void> restartFindServer() async {
// 确保之前的监听已停止
timer?.cancel();
socket.close();
// 重新初始化 socket 和监听
socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 0);
socket.broadcastEnabled = true;
await startFindServer();
}
Future<bool> _findServer({required Duration timeout}) async {
final completer = Completer<bool>();
// 启动 30 秒超时定时器
Future.delayed(timeout, () {
if (!completer.isCompleted) {
timer?.cancel();
completer.complete(false);
}
});
// 轮询发送广播消息
timer = Timer.periodic(state.broadcastInterval, (timer) {
_sendBroadcast();
});
// 监听服务器响应
socket.listen((RawSocketEvent event) async {
if (event == RawSocketEvent.read) {
final datagram = socket.receive();
if (datagram != null) {
final serverResponse = String.fromCharCodes(datagram.data);
Utils().logUtil.printInfo('Found server: $serverResponse');
final serverInfo = serverResponse.split(':');
state.serverIp = serverInfo[0];
state.serverPort = int.parse(serverInfo[1]);
state.isFindingServer = false;
update();
timer?.cancel();
socket.close();
if (!completer.isCompleted) {
completer.complete(true);
}
}
}
});
// 初次发送广播
_sendBroadcast();
return completer.future;
}
// 向服务器发送数据并监听进度
Future<void> sendData(Diary diary) async {
// 创建 FormData 并同步添加 JSON 和文件
dio.FormData formData = dio.FormData();
// 添加 JSON 数据
formData.fields.add(MapEntry('diary', jsonEncode(diary.toJson())));
// 同步添加图片文件
for (var imageName in diary.imageName) {
final filePath = Utils().fileUtil.getRealPath('image', imageName);
formData.files.add(MapEntry(
'image',
await dio.MultipartFile.fromFile(filePath, filename: imageName),
));
}
// 同步添加视频文件
for (var videoName in diary.videoName) {
final filePath = Utils().fileUtil.getRealPath('video', videoName);
formData.files.add(MapEntry(
'video',
await dio.MultipartFile.fromFile(filePath, filename: videoName),
));
}
// 同步添加缩略图文件
for (var videoName in diary.videoName) {
final filePath = Utils().fileUtil.getRealPath('thumbnail', videoName);
formData.files.add(MapEntry(
'thumbnail',
await dio.MultipartFile.fromFile(filePath, filename: 'thumbnail-${videoName.substring(6, 42)}.jpeg'),
));
}
// 同步添加音频文件
for (var audioName in diary.audioName) {
final filePath = Utils().fileUtil.getRealPath('audio', audioName);
formData.files.add(MapEntry(
'audio',
await dio.MultipartFile.fromFile(filePath, filename: audioName),
));
}
final startTime = DateTime.now();
// 发送请求并监听进度
await Utils().httpUtil.dio.post(
'http://${state.serverIp}:${state.serverPort}',
data: formData,
onSendProgress: (int sent, int total) {
final currentTime = DateTime.now();
final timeElapsed = currentTime.difference(startTime).inMilliseconds / 1000;
state.speed.value = sent / timeElapsed;
state.progress.value = sent / total;
},
);
}
}

View File

@@ -0,0 +1,16 @@
import 'package:get/get.dart';
class LocalSendClientState {
int? serverPort;
String? serverIp;
int scanPort = 50001;
Duration broadcastInterval = const Duration(seconds: 3);
RxDouble progress = .0.obs;
RxDouble speed = .0.obs;
bool isFindingServer = false;
LocalSendClientState();
}

View File

@@ -0,0 +1,56 @@
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
import 'package:mood_diary/common/models/isar/diary.dart';
import 'package:mood_diary/utils/utils.dart';
import 'local_send_client_logic.dart';
import 'local_send_client_state.dart';
class LocalSendClientComponent extends StatelessWidget {
const LocalSendClientComponent({super.key});
@override
Widget build(BuildContext context) {
final LocalSendClientLogic logic = Get.put(LocalSendClientLogic());
final LocalSendClientState state = Bind.find<LocalSendClientLogic>().state;
return GetBuilder<LocalSendClientLogic>(
assignId: true,
builder: (_) {
if (state.isFindingServer) {
return const ListTile(
title: Text('查找服务器'),
subtitle: LinearProgressIndicator(),
);
} else if (state.serverIp == null) {
return ListTile(
title: const Text('未找到服务器'),
leading: const FaIcon(FontAwesomeIcons.triangleExclamation),
trailing: FilledButton(
onPressed: () {
logic.restartFindServer();
},
child: const FaIcon(FontAwesomeIcons.repeat)),
);
} else {
return ListTile(
title: Text(state.serverIp!),
subtitle: Obx(() {
return Text(
'${Utils().fileUtil.bytesToUnits(state.speed.value.toInt())['size']}${Utils().fileUtil.bytesToUnits(state.speed.value.toInt())['unit']}/s ${state.progress.value * 100}%');
}),
leading: const FaIcon(FontAwesomeIcons.server),
trailing: FilledButton(
onPressed: () async {
await logic
.sendData((await Utils().isarUtil.getDiaryByID(fastHash('01931742-bd6c-738a-a482-cc8a398b5c0c')))!);
},
child: const FaIcon(FontAwesomeIcons.solidPaperPlane),
),
);
}
},
);
}
}

View File

@@ -0,0 +1,42 @@
import 'package:get/get.dart';
import 'package:network_info_plus/network_info_plus.dart';
import 'local_send_state.dart';
class LocalSendLogic extends GetxController {
final LocalSendState state = LocalSendState();
final networkInfo = NetworkInfo();
@override
void onReady() async {
await getWifiInfo();
super.onReady();
}
@override
void onClose() {
super.onClose();
}
Future<void> getWifiInfo() async {
state.deviceIpAddress = (await networkInfo.getWifiIP()) ?? '无法获取';
state.wifiSSID = (await networkInfo.getWifiName()) ?? '无法获取';
update(['WifiInfo']);
}
// // client
// Future<void> findServer() async {
// state.findingServer.value = true;
// var serverInfo = await localSendClient.findServer();
// if (serverInfo != null) {
// state.serverIp.value = serverInfo['ip'];
// state.serverPort.value = serverInfo['port'];
// state.findingServer.value = false;
// }
// }
void changeType(String value) {
state.type = value;
update(['SegmentButton', 'Panel']);
}
}

View File

@@ -0,0 +1,108 @@
import 'dart:convert';
import 'dart:io';
import 'package:get/get.dart';
import 'package:mood_diary/common/models/isar/diary.dart';
import 'package:mood_diary/utils/utils.dart';
import 'package:network_info_plus/network_info_plus.dart';
import 'package:shelf/shelf.dart' as shelf;
import 'package:shelf/shelf_io.dart';
import 'package:shelf_multipart/shelf_multipart.dart';
import 'local_send_server_state.dart';
class LocalSendServerLogic extends GetxController {
final LocalSendServerState state = LocalSendServerState();
late RawDatagramSocket socket;
HttpServer? httpServer;
late final networkInfo = NetworkInfo();
@override
void onReady() async {
socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, state.scanPort);
state.serverIp = await networkInfo.getWifiIP();
update();
if (state.serverIp != null) {
await startBroadcastListener();
await startServer();
}
super.onReady();
}
@override
void onClose() {
socket.close();
httpServer?.close(force: true);
super.onClose();
}
// 启动UDP广播监听
Future<void> startBroadcastListener() async {
Utils().logUtil.printInfo('Listening for broadcast on port ${state.scanPort}');
socket.listen((RawSocketEvent event) {
if (event == RawSocketEvent.read) {
final datagram = socket.receive();
if (datagram != null) {
final message = String.fromCharCodes(datagram.data);
Utils().logUtil.printInfo('Received broadcast: $message from ${datagram.address.address}');
final response = '${state.serverIp}:${state.transferPort}';
socket.send(response.codeUnits, datagram.address, datagram.port);
}
}
});
}
// 启动HTTP服务器
Future<void> startServer() async {
final handler = const shelf.Pipeline().addMiddleware(shelf.logRequests()).addHandler(_handleRequest);
httpServer = await serve(handler, state.serverIp!, state.transferPort);
Utils().logUtil.printInfo('Server started on http://${state.serverIp}:${state.transferPort}');
}
Future<shelf.Response> _handleRequest(shelf.Request request) async {
late Diary diary;
List<File> images = [];
List<File> videos = [];
List<File> thumbnails = [];
List<File> audios = [];
// 处理表单数据
if (request.formData() case var form?) {
await for (final formData in form.formData) {
final name = formData.name;
// 读取日记 JSON 数据
if (name == 'diary') {
diary = Diary.fromJson(jsonDecode(await formData.part.readString()));
} else if (name == 'image' || name == 'video' || name == 'thumbnail' || name == 'audio') {
if (formData.filename != null) {
final tempFile = File(Utils().fileUtil.getCachePath(formData.filename!));
final sink = tempFile.openWrite();
await formData.part.pipe(sink);
await sink.close();
// 分类文件
if (name == 'image') images.add(tempFile);
if (name == 'video') videos.add(tempFile);
if (name == 'thumbnail') thumbnails.add(tempFile);
if (name == 'audio') audios.add(tempFile);
}
}
}
}
return shelf.Response.ok('Data and files received successfully');
}
// 辅助函数,用于按块保存文件并调用进度更新回调
Future<File> _saveFileWithProgress(Stream<List<int>> stream, String filename, String? mimeType, int contentLength,
void Function(int chunkLength) onProgress) async {
final file = File('/path/to/save/$filename');
final sink = file.openWrite();
await for (final chunk in stream) {
sink.add(chunk);
onProgress(chunk.length); // 调用进度更新回调
}
await sink.close();
return file;
}
}

View File

@@ -0,0 +1,14 @@
import 'package:get/get.dart';
class LocalSendServerState {
String? serverIp;
int scanPort = 50001;
int transferPort = 54321;
RxDouble progress = .0.obs;
RxDouble speed = .0.obs;
LocalSendServerState();
}

View File

@@ -0,0 +1,25 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'local_send_server_logic.dart';
import 'local_send_server_state.dart';
class LocalSendServerComponent extends StatelessWidget {
const LocalSendServerComponent({super.key});
@override
Widget build(BuildContext context) {
final LocalSendServerLogic logic = Get.put(LocalSendServerLogic());
final LocalSendServerState state = Bind.find<LocalSendServerLogic>().state;
return GetBuilder<LocalSendServerLogic>(
assignId: true,
builder: (_) {
return ListTile(
title: const Text('服务器已启动在'),
subtitle: Text('${state.serverIp}:${state.transferPort}'),
);
},
);
}
}

View File

@@ -0,0 +1,19 @@
import 'package:get/get.dart';
class LocalSendState {
String wifiSSID = '';
String deviceIpAddress = '';
int transferPort = 54321;
int scanPort = 50001;
RxBool findingServer = false.obs;
RxString serverIp = ''.obs;
RxnInt serverPort = RxnInt();
String type = 'send';
LocalSendState();
}

View File

@@ -0,0 +1,126 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:mood_diary/components/local_send/local_send_client/local_send_client_view.dart';
import 'package:mood_diary/components/local_send/local_send_server/local_send_server_view.dart';
import 'local_send_logic.dart';
import 'local_send_state.dart';
class LocalSendComponent extends StatelessWidget {
const LocalSendComponent({super.key});
@override
Widget build(BuildContext context) {
final LocalSendLogic logic = Get.put(LocalSendLogic());
final LocalSendState state = Bind.find<LocalSendLogic>().state;
final textStyle = Theme.of(context).textTheme;
Widget buildWifiInfo() {
return Column(
spacing: 8.0,
children: [
Text(
'请检查以下信息,确保两台设备在同一局域网下',
style: textStyle.titleSmall,
),
GetBuilder<LocalSendLogic>(
id: 'WifiInfo',
builder: (_) {
return Wrap(
spacing: 8.0,
children: [
ActionChip(
onPressed: () {
logic.getWifiInfo();
},
label: Text('SSID : ${state.wifiSSID}'),
),
ActionChip(
onPressed: () {
logic.getWifiInfo();
},
label: Text('IP : ${state.deviceIpAddress}'),
),
],
);
}),
],
);
}
Widget buildPortInfo() {
return Column(
spacing: 8.0,
children: [
Text(
'如果您不知道这是什么,请不要修改',
style: textStyle.titleSmall,
),
Wrap(
spacing: 8.0,
children: [
ActionChip(
onPressed: () {},
label: Text('扫描端口: ${state.scanPort}'),
),
ActionChip(
onPressed: () {},
label: Text('传输端口: ${state.transferPort}'),
),
],
),
],
);
}
return GetBuilder<LocalSendLogic>(
assignId: true,
builder: (_) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
spacing: 8.0,
children: [
Wrap(
spacing: 8.0,
runSpacing: 8.0,
alignment: WrapAlignment.spaceEvenly,
children: [buildWifiInfo(), buildPortInfo()],
),
GetBuilder<LocalSendLogic>(
id: 'SegmentButton',
builder: (_) {
return SegmentedButton<String>(
segments: const [
ButtonSegment(
value: 'send',
icon: Icon(Icons.send),
label: Text('发送'),
),
ButtonSegment(
value: 'receive',
icon: Icon(Icons.move_to_inbox_rounded),
label: Text('接收'),
),
],
selected: {state.type},
onSelectionChanged: (newSelection) {
logic.changeType(newSelection.first);
},
);
}),
GetBuilder<LocalSendLogic>(
id: 'Panel',
builder: (_) {
return state.type == 'send' ? const LocalSendClientComponent() : const LocalSendServerComponent();
}),
],
),
);
},
);
}
}

View File

@@ -47,8 +47,7 @@ class RecordSheetLogic extends GetxController with GetTickerProviderStateMixin {
}
Future<void> startRecorder() async {
if (await Utils().permissionUtil.checkPermission(Permission.microphone) &&
await Utils().permissionUtil.checkPermission(Permission.bluetoothConnect)) {
if (await Utils().permissionUtil.checkPermission(Permission.microphone)) {
await animationController.forward();
state.isRecording.value = true;
state.isStarted.value = true;

View File

@@ -12,7 +12,6 @@ import 'package:intl/find_locale.dart';
import 'package:media_kit/media_kit.dart';
import 'package:mood_diary/router/app_pages.dart';
import 'package:mood_diary/router/app_routes.dart';
import 'package:mood_diary/utils/channel.dart';
import 'package:mood_diary/utils/utils.dart';
Future<void> initSystem() async {
@@ -35,8 +34,6 @@ void platFormOption() {
if (Platform.isAndroid) {
//设置高刷
unawaited(FlutterDisplayMode.setHighRefreshRate());
//设置状态栏沉浸
unawaited(ViewChannel.setSystemUIVisibility());
}
if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) {
doWhenWindowReady(() {

View File

@@ -0,0 +1,19 @@
import 'package:get/get.dart';
import 'backup_sync_state.dart';
class BackupSyncLogic extends GetxController {
final BackupSyncState state = BackupSyncState();
@override
void onReady() {
// TODO: implement onReady
super.onReady();
}
@override
void onClose() {
// TODO: implement onClose
super.onClose();
}
}

View File

@@ -0,0 +1,5 @@
class BackupSyncState {
BackupSyncState() {
///Initialize variables
}
}

View File

@@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:mood_diary/components/local_send/local_send_view.dart';
import 'backup_sync_logic.dart';
import 'backup_sync_state.dart';
class BackupSyncPage extends StatelessWidget {
const BackupSyncPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final BackupSyncLogic logic = Get.put(BackupSyncLogic());
final BackupSyncState state = Bind.find<BackupSyncLogic>().state;
return Scaffold(
appBar: AppBar(
title: const Text('备份与同步'),
),
body: ListView(
children: const [
ExpansionTile(
leading: Icon(Icons.wifi_tethering_rounded),
title: Text('局域网传输'),
children: [LocalSendComponent()],
),
ExpansionTile(
leading: Icon(Icons.backup_rounded),
title: Text('WebDav'),
children: [],
)
],
),
);
}
}

View File

@@ -15,7 +15,7 @@ class CalendarPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final logic = Bind.find<CalendarLogic>();
final logic = Get.put(CalendarLogic());
final state = Bind.find<CalendarLogic>().state;
final colorScheme = Theme.of(context).colorScheme;
final i18n = AppLocalizations.of(context)!;

View File

@@ -12,8 +12,7 @@ class DiaryLogic extends GetxController with GetTickerProviderStateMixin {
final DiaryState state = DiaryState();
//初始化tab控制器长度加一由于有一个默认分类
late TabController tabController =
TabController(length: state.categoryList.length + 1, vsync: this);
late TabController tabController = TabController(length: state.categoryList.length + 1, vsync: this);
late HomeLogic homeLogic = Bind.find<HomeLogic>();
@@ -43,10 +42,9 @@ class DiaryLogic extends GetxController with GetTickerProviderStateMixin {
/// 在动态更新分类后要重新监听
void _tabBarListener() {
if (tabController.indexIsChanging) return;
_checkPageChange();
checkPageChange();
// 检查是否显示顶部内容
_checkShowTop();
homeLogic.resetNavigatorBar();
}
@@ -68,9 +66,7 @@ class DiaryLogic extends GetxController with GetTickerProviderStateMixin {
if (tabController.index == 0) {
await Bind.find<DiaryTabViewLogic>(tag: 'default').paginationDiary();
} else {
await Bind.find<DiaryTabViewLogic>(
tag: state.categoryList[tabController.index - 1].id)
.paginationDiary();
await Bind.find<DiaryTabViewLogic>(tag: state.categoryList[tabController.index - 1].id).paginationDiary();
}
}
}
@@ -99,14 +95,10 @@ class DiaryLogic extends GetxController with GetTickerProviderStateMixin {
/// 需要在以下情况调用
/// 1. tab bar 修改
/// 2. update ho
void _checkPageChange() {
void checkPageChange() {
state.currentTabBarIndex = tabController.index;
// 获取当前分类ID若为默认分类设为 'default'
String categoryId = state.currentTabBarIndex == 0
? 'default'
: state.categoryList[state.currentTabBarIndex - 1].id;
String categoryId = state.currentTabBarIndex == 0 ? 'default' : state.categoryList[state.currentTabBarIndex - 1].id;
// 遍历 keyMap更新每个分类的状态
state.keyMap.forEach((k, v) {
v.currentState?.onPageChange(k == categoryId);
@@ -129,15 +121,13 @@ class DiaryLogic extends GetxController with GetTickerProviderStateMixin {
}
} else {
//查找分类对应的位置,加一是因为默认分类占了一个
tabViewIndex =
state.categoryList.indexWhere((e) => e.id == categoryId) + 1;
tabViewIndex = state.categoryList.indexWhere((e) => e.id == categoryId) + 1;
if (jump && tabController.index != 0) {
tabController.animateTo(tabViewIndex);
}
}
//如果控制器已经存在,重新获取,如果不存在,不需要任何操作
if (tabViewIndex != 0 &&
Bind.isRegistered<DiaryTabViewLogic>(tag: categoryId)) {
if (tabViewIndex != 0 && Bind.isRegistered<DiaryTabViewLogic>(tag: categoryId)) {
await Bind.find<DiaryTabViewLogic>(tag: categoryId).getDiary();
}
await Bind.find<DiaryTabViewLogic>(tag: 'default').getDiary();
@@ -154,9 +144,8 @@ class DiaryLogic extends GetxController with GetTickerProviderStateMixin {
state.categoryList = await Utils().isarUtil.getAllCategoryAsync();
// 移除 Map 中不再存在的 Category id
state.keyMap.removeWhere((k, v) =>
!state.categoryList.map((category) => category.id).contains(k) &&
k != 'default');
state.keyMap
.removeWhere((k, v) => !state.categoryList.map((category) => category.id).contains(k) && k != 'default');
// 为新的 Category 添加新的 GlobalKey
for (var category in state.categoryList) {
@@ -173,31 +162,22 @@ class DiaryLogic extends GetxController with GetTickerProviderStateMixin {
//重新创建控制器
tabController.removeListener(_tabBarListener);
tabController =
TabController(length: state.categoryList.length + 1, vsync: this);
tabController = TabController(length: state.categoryList.length + 1, vsync: this);
tabController.addListener(_tabBarListener);
update();
_checkPageChange();
update(['All']);
checkPageChange();
}
//切换视图模式
Future<void> changeViewMode(ViewModeType viewModeType) async {
state.viewModeType = viewModeType;
if (state.currentTabBarIndex == 0) {
Bind.find<DiaryTabViewLogic>(tag: 'default').update(['TabView']);
} else {
Bind.find<DiaryTabViewLogic>(
tag: state.categoryList[state.currentTabBarIndex - 1].id)
.update(['TabView']);
}
homeLogic.state.isToTopShow = false;
homeLogic.update(['Fab']);
update(['TabBarView']);
_checkShowTop();
await Utils().prefUtil.setValue<int>('homeViewMode', viewModeType.number);
}
// 回到顶部函数
Future<void> toTop() async {
await state.innerController.animateTo(0.0,
duration: const Duration(milliseconds: 200), curve: Curves.linear);
await state.innerController.animateTo(0.0, duration: const Duration(milliseconds: 200), curve: Curves.easeInOut);
}
}

View File

@@ -1,3 +1,4 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:get/get.dart';
@@ -14,7 +15,7 @@ class DiaryPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final logic = Bind.find<DiaryLogic>();
final logic = Get.put(DiaryLogic());
final state = Bind.find<DiaryLogic>().state;
final colorScheme = Theme.of(context).colorScheme;
final i18n = AppLocalizations.of(context)!;
@@ -37,6 +38,7 @@ class DiaryPage extends StatelessWidget {
tabAlignment: TabAlignment.start,
indicatorSize: TabBarIndicatorSize.label,
splashFactory: NoSplash.splashFactory,
dragStartBehavior: DragStartBehavior.start,
indicator: UnderlineTabIndicator(
borderSide: BorderSide(
color: colorScheme.primary,
@@ -63,83 +65,96 @@ class DiaryPage extends StatelessWidget {
Widget buildTabBarView() {
List<Widget> allViews = [];
// 添加全部日记页面
allViews.add(buildDiaryView(0, state.keyMap['default'], null));
// 添加分类日记页面
allViews.addAll(List.generate(state.categoryList.length, (index) {
return buildDiaryView(
index + 1,
state.keyMap[state.categoryList[index].id],
state.categoryList[index].id);
return buildDiaryView(index + 1, state.keyMap[state.categoryList[index].id], state.categoryList[index].id);
}));
return TabBarView(
controller: logic.tabController,
children: allViews,
return NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification) {
if (notification.metrics.axis == Axis.horizontal) {
logic.checkPageChange();
}
return true;
},
child: TabBarView(
controller: logic.tabController,
dragStartBehavior: DragStartBehavior.start,
children: allViews,
),
);
}
return NestedScrollView(
key: state.nestedScrollKey,
headerSliverBuilder: (context, _) {
return [
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar(
title: Text(
state.customTitleName.isNotEmpty
? state.customTitleName
: i18n.appName,
overflow: TextOverflow.ellipsis,
),
actions: [
IconButton(
onPressed: () {
showModalBottomSheet(
context: context,
showDragHandle: true,
useSafeArea: true,
builder: (context) {
return const SearchSheetComponent();
});
},
icon: const Icon(Icons.search),
tooltip: i18n.diaryPageSearchButton,
),
PopupMenuButton(
offset: const Offset(0, 46),
tooltip: i18n.diaryPageViewModeButton,
itemBuilder: (context) {
return <PopupMenuEntry<String>>[
CheckedPopupMenuItem(
checked: state.viewModeType == ViewModeType.list,
onTap: () async {
await logic.changeViewMode(ViewModeType.list);
return GetBuilder<DiaryLogic>(
assignId: true,
builder: (_) {
return NestedScrollView(
key: state.nestedScrollKey,
headerSliverBuilder: (context, _) {
return [
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar(
title: GetBuilder<DiaryLogic>(
id: 'Title',
builder: (_) {
return Text(
state.customTitleName.isNotEmpty ? state.customTitleName : i18n.appName,
overflow: TextOverflow.ellipsis,
);
}),
pinned: true,
actions: [
IconButton(
onPressed: () {
showModalBottomSheet(
context: context,
showDragHandle: true,
useSafeArea: true,
builder: (context) {
return const SearchSheetComponent();
});
},
child: Text(i18n.diaryViewModeList),
icon: const Icon(Icons.search),
tooltip: i18n.diaryPageSearchButton,
),
const PopupMenuDivider(),
CheckedPopupMenuItem(
checked: state.viewModeType == ViewModeType.grid,
onTap: () async {
await logic.changeViewMode(ViewModeType.grid);
PopupMenuButton(
offset: const Offset(0, 46),
tooltip: i18n.diaryPageViewModeButton,
itemBuilder: (context) {
return <PopupMenuEntry<String>>[
CheckedPopupMenuItem(
checked: state.viewModeType == ViewModeType.list,
onTap: () async {
await logic.changeViewMode(ViewModeType.list);
},
child: Text(i18n.diaryViewModeList),
),
const PopupMenuDivider(),
CheckedPopupMenuItem(
checked: state.viewModeType == ViewModeType.grid,
onTap: () async {
await logic.changeViewMode(ViewModeType.grid);
},
child: Text(i18n.diaryViewModeGrid),
),
];
},
child: Text(i18n.diaryViewModeGrid),
),
];
},
],
bottom: PreferredSize(preferredSize: const Size.fromHeight(46.0), child: buildTabBar()),
),
),
],
bottom: PreferredSize(
preferredSize: const Size.fromHeight(46.0),
child: buildTabBar()),
),
),
];
},
body: buildTabBarView(),
);
];
},
body: GetBuilder<DiaryLogic>(
id: 'TabBarView',
builder: (_) {
return buildTabBarView();
}),
);
});
}
}

View File

@@ -17,7 +17,7 @@ class MediaPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final logic = Bind.find<MediaLogic>();
final logic = Get.put(MediaLogic());
final state = Bind.find<MediaLogic>().state;
final i18n = AppLocalizations.of(context)!;

View File

@@ -15,8 +15,7 @@ class SettingLogic extends GetxController {
final SettingState state = SettingState();
late final homeLogic = Bind.find<HomeLogic>();
late TextEditingController textEditingController =
TextEditingController(text: state.customTitle);
late TextEditingController textEditingController = TextEditingController(text: state.customTitle);
@override
void onInit() {
@@ -88,6 +87,10 @@ class SettingLogic extends GetxController {
Get.toNamed(AppRoutes.diarySettingPage);
}
void toBackupAndSyncPage() {
Get.toNamed(AppRoutes.backupSyncPage);
}
void cancelCustomTitle() {
textEditingController.clear();
Get.backLegacy();
@@ -97,9 +100,7 @@ class SettingLogic extends GetxController {
if (textEditingController.text.isNotEmpty) {
state.customTitle = textEditingController.text;
update(['CustomTitle']);
await Utils()
.prefUtil
.setValue<String>('customTitleName', textEditingController.text);
await Utils().prefUtil.setValue<String>('customTitleName', textEditingController.text);
Get.backLegacy();
textEditingController.clear();
Utils().noticeUtil.showToast('重启应用后生效');
@@ -129,8 +130,7 @@ class SettingLogic extends GetxController {
//导入
Future<void> import() async {
Get.backLegacy();
FilePickerResult? result = await FilePicker.platform
.pickFiles(allowedExtensions: ['zip'], type: FileType.custom);
FilePickerResult? result = await FilePicker.platform.pickFiles(allowedExtensions: ['zip'], type: FileType.custom);
if (result != null) {
Utils().noticeUtil.showToast('数据导入中,请不要离开页面');
await Utils().fileUtil.extractFile(result.files.single.path!);

View File

@@ -15,24 +15,14 @@ class SettingPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final logic = Bind.find<SettingLogic>();
final logic = Get.put(SettingLogic());
final state = Bind.find<SettingLogic>().state;
final textStyle = Theme.of(context).textTheme;
final colorScheme = Theme.of(context).colorScheme;
final i18n = AppLocalizations.of(context)!;
Widget buildManager() {
return Column(
children: [
ListTile(
title: Text(
'管理',
style: textStyle.titleLarge!.copyWith(color: colorScheme.primary, fontWeight: FontWeight.bold),
),
),
const DashboardComponent(),
],
);
return const DashboardComponent();
}
Widget buildData() {
@@ -56,6 +46,14 @@ class SettingPage extends StatelessWidget {
},
leading: const Icon(Icons.delete_outline),
),
ListTile(
title: const Text('备份与同步'),
trailing: const Icon(Icons.chevron_right),
onTap: () {
logic.toBackupAndSyncPage();
},
leading: const Icon(Icons.sync),
),
ListTile(
title: Text(i18n.settingExport),
onTap: () {

View File

@@ -4,6 +4,8 @@ import 'package:mood_diary/pages/about/about_view.dart';
import 'package:mood_diary/pages/agreement/agreement_view.dart';
import 'package:mood_diary/pages/analyse/analyse_logic.dart';
import 'package:mood_diary/pages/analyse/analyse_view.dart';
import 'package:mood_diary/pages/backup_sync/backup_sync_logic.dart';
import 'package:mood_diary/pages/backup_sync/backup_sync_view.dart';
import 'package:mood_diary/pages/category_manager/category_manager_logic.dart';
import 'package:mood_diary/pages/category_manager/category_manager_view.dart';
import 'package:mood_diary/pages/diary_details/diary_details_view.dart';
@@ -15,12 +17,8 @@ import 'package:mood_diary/pages/edit/edit_logic.dart';
import 'package:mood_diary/pages/edit/edit_view.dart';
import 'package:mood_diary/pages/font/font_logic.dart';
import 'package:mood_diary/pages/font/font_view.dart';
import 'package:mood_diary/pages/home/calendar/calendar_logic.dart';
import 'package:mood_diary/pages/home/diary/diary_logic.dart';
import 'package:mood_diary/pages/home/home_logic.dart';
import 'package:mood_diary/pages/home/home_view.dart';
import 'package:mood_diary/pages/home/media/media_logic.dart';
import 'package:mood_diary/pages/home/setting/setting_logic.dart';
import 'package:mood_diary/pages/image/image_logic.dart';
import 'package:mood_diary/pages/image/image_view.dart';
import 'package:mood_diary/pages/laboratory/laboratory_logic.dart';
@@ -60,11 +58,6 @@ class AppPages {
page: () => const HomePage(),
binds: [
Bind.lazyPut(() => HomeLogic()),
Bind.lazyPut(() => DiaryLogic()),
Bind.lazyPut(() => CalendarLogic()),
Bind.lazyPut(() => MediaLogic()),
//Bind.lazyPut(() => AssistantLogic()),
Bind.lazyPut(() => SettingLogic()),
],
),
//分析
@@ -175,5 +168,10 @@ class AppPages {
page: () => const DiarySettingPage(),
binds: [Bind.lazyPut(() => DiarySettingLogic())],
),
GetPage(
name: AppRoutes.backupSyncPage,
page: () => const BackupSyncPage(),
binds: [Bind.lazyPut(() => BackupSyncLogic())],
),
];
}

View File

@@ -69,4 +69,7 @@ abstract class AppRoutes {
//日记个性化
static const diarySettingPage = '/diarySetting';
//备份与同步页
static const backupSyncPage = '/backupSync';
}

27
lib/utils/aes_util.dart Normal file
View File

@@ -0,0 +1,27 @@
import 'dart:convert';
import 'package:encrypt/encrypt.dart' as encrypt;
class AESUtil {
final encrypt.Key _key;
final encrypt.IV _iv;
// 初始化时设置密钥和初始化向量IV
AESUtil(String key, String iv)
: _key = encrypt.Key.fromUtf8(key.padRight(32, ' ')),
// 32字节密钥
_iv = encrypt.IV.fromUtf8(iv.padRight(16, ' ')); // 16字节IV
// 加密数据
String encryptData(String data) {
final encrypter = encrypt.Encrypter(encrypt.AES(_key));
final encrypted = encrypter.encrypt(data, iv: _iv);
return encrypted.base64; // 返回加密后的数据
}
// 解密数据
String decryptData(String encryptedData) {
final encrypter = encrypt.Encrypter(encrypt.AES(_key));
final decrypted = encrypter.decrypt64(encryptedData, iv: _iv);
return decrypted; // 返回解密后的数据
}
}

View File

@@ -1,13 +1,5 @@
import 'package:flutter/services.dart';
class ViewChannel {
static const MethodChannel _channel = MethodChannel('view_channel');
static setSystemUIVisibility() async {
await _channel.invokeMethod('setSystemUIVisibility');
}
}
class FontChannel {
static const MethodChannel _channel = MethodChannel('font_channel');

View File

@@ -1,6 +1,8 @@
import 'package:isar/isar.dart';
import 'package:latlong2/latlong.dart';
import 'package:mood_diary/common/models/isar/category.dart';
import 'package:mood_diary/common/models/isar/diary.dart';
import 'package:mood_diary/common/models/map.dart';
import 'package:mood_diary/utils/utils.dart';
import 'package:path/path.dart';
import 'package:uuid/uuid.dart';
@@ -237,6 +239,24 @@ class IsarUtil {
isar.close();
}
// 获取用于地图显示的对象
Future<List<DiaryMapItem>> getAllMapItem() async {
List<DiaryMapItem> res = [];
/// 所有的日记
/// 要满足以下条件
/// 1. 有定位坐标
/// 2. 有照片
/// 3. show
var diaries =
await _isar.diarys.where().showEqualTo(true).positionIsNotEmpty().imageNameIsNotEmpty().findAllAsync();
for (var diary in diaries) {
res.add(DiaryMapItem(LatLng(double.parse(diary.position[0]), double.parse(diary.position[1])), diary.isarId,
diary.imageName.first));
}
return res;
}
//构建搜索
void buildSearch(String dir) async {
var isar = Isar.open(

View File

@@ -1,64 +0,0 @@
import 'dart:convert';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:mood_diary/utils/utils.dart';
class LocalLanClient {
final dio = Utils().httpUtil.dio;
final int port;
RawDatagramSocket? socket;
LocalLanClient({this.port = 4040});
/// 初始化UDP套接字
Future<void> initSocket() async {
socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, port);
socket!.broadcastEnabled = true;
print("UDP socket initialized on port $port");
}
/// 广播设备信息
void broadcastPresence() {
final message = utf8.encode("LocalLanClient: device here");
socket!.send(message, InternetAddress("224.0.0.1"), port);
print("Presence broadcasted");
}
/// 监听其他设备的广播消息
void listenForDevices() {
socket!.listen((RawSocketEvent event) {
if (event == RawSocketEvent.read) {
final packet = socket!.receive();
if (packet != null) {
final message = utf8.decode(packet.data);
print("Discovered device at ${packet.address.address}: $message");
}
}
});
}
/// 发送文件到指定设备IP
Future<void> sendFile(String filePath, String deviceIp) async {
final file = File(filePath);
final formData = FormData.fromMap({
"file": await MultipartFile.fromFile(file.path, filename: file.path.split('/').last),
});
try {
final response = await dio.post(
'http://$deviceIp:$port/upload',
data: formData,
);
print("File sent successfully: ${response.data}");
} catch (e) {
print("Error sending file: $e");
}
}
/// 关闭UDP套接字
void closeSocket() {
socket?.close();
print("Socket closed");
}
}

View File

@@ -1,58 +0,0 @@
import 'dart:io';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as shelf_io;
import 'package:shelf_multipart/shelf_multipart.dart';
class LocalLanServer {
final int port;
HttpServer? _server;
LocalLanServer({this.port = 4040});
/// 启动文件服务器
Future<void> start() async {
// 配置路由和中间件
final handler = const Pipeline()
.addMiddleware(logRequests()) // 日志中间件
.addHandler(_router); // 路由处理器
// 启动 Shelf 服务器
_server = await shelf_io.serve(handler, InternetAddress.anyIPv4, port);
print("Shelf server started on port $port");
}
/// 路由处理器
Future<Response> _router(Request request) async {
if (request.url.path == 'upload' && request.method == 'POST') {
return _handleFileUpload(request);
} else {
return Response.notFound('Not Found');
}
}
/// 处理文件上传请求
Future<Response> _handleFileUpload(Request request) async {
try {
final multipart = request.multipart();
await for (final part in multipart!.parts) {
final fileName = 'received_${DateTime.now().millisecondsSinceEpoch}.file';
final file = File('/path/to/save/$fileName'); // 替换为实际保存路径
final sink = file.openWrite();
await part.pipe(sink); // 将文件写入本地
await sink.close();
print("File received: ${file.path}");
}
return Response.ok('File received successfully');
} catch (e) {
print("Error receiving file: $e");
return Response.internalServerError(body: 'File upload failed');
}
}
/// 停止服务器
Future<void> stop() async {
await _server?.close();
print("Shelf server stopped");
}
}

View File

@@ -18,9 +18,9 @@ class MediaUtil {
List<String> nameList = [];
for (var imageFile in imageFileList) {
//生成新的名字
var imageName = 'image-${const Uuid().v7()}.webp';
var imageName = 'image-${const Uuid().v7()}.png';
nameList.add(imageName);
await _compressAndSaveImage(imageFile, Utils().fileUtil.getRealPath('image', imageName), CompressFormat.webp);
await _compressAndSaveImage(imageFile, Utils().fileUtil.getRealPath('image', imageName), CompressFormat.png);
}
return nameList;
}

View File

@@ -1,9 +1,14 @@
import 'dart:io';
import 'package:mood_diary/utils/utils.dart';
import 'package:permission_handler/permission_handler.dart';
class PermissionUtil {
//权限申请
Future<bool> checkPermission(Permission permission) async {
if (Platform.isMacOS) {
return true;
}
//检查当前权限
final status = await permission.status;
//如果还没有授权或者拒绝过

View File

@@ -6,8 +6,6 @@ import 'package:mood_diary/utils/data/supabase.dart';
import 'package:mood_diary/utils/file_util.dart';
import 'package:mood_diary/utils/http_util.dart';
import 'package:mood_diary/utils/layout_util.dart';
import 'package:mood_diary/utils/localsend/client.dart';
import 'package:mood_diary/utils/localsend/server.dart';
import 'package:mood_diary/utils/log_util.dart';
import 'package:mood_diary/utils/media_util.dart';
import 'package:mood_diary/utils/notice_util.dart';
@@ -59,8 +57,4 @@ class Utils {
late final PrefUtil prefUtil = PrefUtil();
late final SupabaseUtil supabaseUtil = SupabaseUtil();
late final LocalLanClient localLanClient = LocalLanClient();
late final LocalLanServer localLanServer = LocalLanServer();
}

View File

@@ -18,6 +18,7 @@ import isar_flutter_libs
import local_auth_darwin
import media_kit_libs_macos_video
import media_kit_video
import network_info_plus
import objectbox_flutter_libs
import package_info_plus
import path_provider_foundation
@@ -44,6 +45,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FLALocalAuthPlugin.register(with: registry.registrar(forPlugin: "FLALocalAuthPlugin"))
MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin"))
MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin"))
NetworkInfoPlusPlugin.register(with: registry.registrar(forPlugin: "NetworkInfoPlusPlugin"))
ObjectboxFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "ObjectboxFlutterLibsPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))

View File

@@ -30,6 +30,8 @@ PODS:
- FlutterMacOS
- media_kit_video (0.0.1):
- FlutterMacOS
- network_info_plus (0.0.1):
- FlutterMacOS
- ObjectBox (4.0.1)
- objectbox_flutter_libs (0.0.1):
- FlutterMacOS
@@ -76,6 +78,7 @@ DEPENDENCIES:
- media_kit_libs_macos_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos`)
- media_kit_native_event_loop (from `Flutter/ephemeral/.symlinks/plugins/media_kit_native_event_loop/macos`)
- media_kit_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos`)
- network_info_plus (from `Flutter/ephemeral/.symlinks/plugins/network_info_plus/macos`)
- objectbox_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/objectbox_flutter_libs/macos`)
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
@@ -124,6 +127,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/media_kit_native_event_loop/macos
media_kit_video:
:path: Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos
network_info_plus:
:path: Flutter/ephemeral/.symlinks/plugins/network_info_plus/macos
objectbox_flutter_libs:
:path: Flutter/ephemeral/.symlinks/plugins/objectbox_flutter_libs/macos
package_info_plus:
@@ -153,7 +158,7 @@ SPEC CHECKSUMS:
app_links: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a
audioplayers_darwin: dcad41de4fbd0099cb3749f7ab3b0cb8f70b810c
bitsdojo_window_macos: 44e3b8fe3dd463820e0321f6256c5b1c16bb6a00
device_info_plus: 74e614483d05c89290d30a4c8feae15d555f7427
device_info_plus: 1b14eed9bf95428983aed283a8d51cce3d8c4215
dynamic_color: 2eaa27267de1ca20d879fbd6e01259773fb1670f
fc_native_video_thumbnail: 927d4dcfd4c7e9f2cc1a20bb52dfee83de3792c2
file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d
@@ -165,14 +170,15 @@ SPEC CHECKSUMS:
media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82
media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5
media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5
network_info_plus: 2cb02d8435635eae13b3b79279681985121cf30c
ObjectBox: 0bc4bb75eea85f6af06b369148b334c2056bbc29
objectbox_flutter_libs: 769e6f44f7381c8a8e46a2ed5c71c6068bb476f7
package_info_plus: f5790acc797bf17c3e959e9d6cf162cc68ff7523
package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
quill_native_bridge_macos: f90985c5269ac7ba84d933605b463d96e5f544fe
record_darwin: a0d515a0ef78c440c123ea3ac76184c9927a94d6
screen_brightness_macos: 2d6d3af2165592d9a55ffcd95b7550970e41ebda
share_plus: fd717ef89a2801d3491e737630112b80c310640e
share_plus: 1fa619de8392a4398bfaf176d441853922614e89
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite_darwin: a553b1fd6fe66f53bbb0fe5b4f5bab93f08d7a13
tflite_flutter: b186ea3c62c076a90652f47c01005fe36c75171e

View File

@@ -78,6 +78,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.6.0"
asn1lib:
dependency: transitive
description:
name: asn1lib
sha256: "4bae5ae63e6d6dd17c4aac8086f3dec26c0236f6a0f03416c6c19d830c367cf5"
url: "https://pub.dev"
source: hosted
version: "1.5.8"
async:
dependency: transitive
description:
@@ -426,10 +434,10 @@ packages:
dependency: "direct main"
description:
name: device_info_plus
sha256: c4af09051b4f0508f6c1dc0a5c085bf014d5c9a4a0678ce1799c2b4d716387a0
sha256: f545ffbadee826f26f2e1a0f0cbd667ae9a6011cc0f77c0f8f00a969655e6e95
url: "https://pub.dev"
source: hosted
version: "11.1.0"
version: "11.1.1"
device_info_plus_platform_interface:
dependency: transitive
description:
@@ -478,6 +486,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.7.0"
encrypt:
dependency: "direct main"
description:
name: encrypt
sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2"
url: "https://pub.dev"
source: hosted
version: "5.0.3"
equatable:
dependency: transitive
description:
@@ -724,10 +740,10 @@ packages:
dependency: transitive
description:
name: flutter_keyboard_visibility_temp_fork
sha256: e342172aaa6173a661e822c85a005f8c5d0a04a1d263e00cb9f9155adab9cb7c
sha256: "2d94acecfc170d244157821cc67e784f60972677aac94a6672626a5d6b2dc537"
url: "https://pub.dev"
source: hosted
version: "0.1.1"
version: "0.1.3"
flutter_keyboard_visibility_windows:
dependency: transitive
description:
@@ -1385,8 +1401,32 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.0"
objectbox:
network_info_plus:
dependency: "direct main"
description:
name: network_info_plus
sha256: bf9e39e523e9951d741868dc33ac386b0bc24301e9b7c8a7d60dbc34879150a8
url: "https://pub.dev"
source: hosted
version: "6.1.1"
network_info_plus_platform_interface:
dependency: transitive
description:
name: network_info_plus_platform_interface
sha256: b7f35f4a7baef511159e524499f3c15464a49faa5ec10e92ee0bce265e664906
url: "https://pub.dev"
source: hosted
version: "2.0.1"
nm:
dependency: transitive
description:
name: nm
sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254"
url: "https://pub.dev"
source: hosted
version: "0.5.0"
objectbox:
dependency: "direct main"
description:
name: objectbox
sha256: ea823f4bf1d0a636e7aa50b43daabb64dd0fbd80b85a033016ccc1bc4f76f432
@@ -1421,10 +1461,10 @@ packages:
dependency: "direct main"
description:
name: package_info_plus
sha256: df3eb3e0aed5c1107bb0fdb80a8e82e778114958b1c5ac5644fb1ac9cae8a998
sha256: da8d9ac8c4b1df253d1a328b7bf01ae77ef132833479ab40763334db13b91cce
url: "https://pub.dev"
source: hosted
version: "8.1.0"
version: "8.1.1"
package_info_plus_platform_interface:
dependency: transitive
description:
@@ -1569,6 +1609,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.8"
pointycastle:
dependency: transitive
description:
name: pointycastle
sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe"
url: "https://pub.dev"
source: hosted
version: "3.9.1"
polylabel:
dependency: transitive
description:
@@ -1685,10 +1733,10 @@ packages:
dependency: transitive
description:
name: quill_native_bridge_web
sha256: "59d673b946ecb8dbcdd387a4957de12f0263690bdbe159832355855ac7a45de9"
sha256: bb3ab017fdb9b60a29cac0bce3acfd48396d13c1bd0499c97af112c84937b4d1
url: "https://pub.dev"
source: hosted
version: "0.0.1-dev.4"
version: "0.0.1-dev.5"
quill_native_bridge_windows:
dependency: transitive
description:
@@ -1853,10 +1901,10 @@ packages:
dependency: "direct main"
description:
name: share_plus
sha256: "3af2cda1752e5c24f2fc04b6083b40f013ffe84fb90472f30c6499a9213d5442"
sha256: "9c9bafd4060728d7cdb2464c341743adbd79d327cb067ec7afb64583540b47c8"
url: "https://pub.dev"
source: hosted
version: "10.1.1"
version: "10.1.2"
share_plus_platform_interface:
dependency: transitive
description:
@@ -1869,10 +1917,10 @@ packages:
dependency: "direct main"
description:
name: shared_preferences
sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051"
sha256: "95f9997ca1fb9799d494d0cb2a780fd7be075818d59f00c43832ed112b158a82"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
version: "2.3.3"
shared_preferences_android:
dependency: transitive
description:
@@ -2002,10 +2050,10 @@ packages:
dependency: transitive
description:
name: sqflite
sha256: "79a297dc3cc137e758c6a4baf83342b039e5a6d2436fcdf3f96a00adaaf2ad62"
sha256: "2d7299468485dca85efeeadf5d38986909c5eb0cd71fd3db2c2f000e6c9454bb"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
version: "2.4.1"
sqflite_android:
dependency: transitive
description:
@@ -2026,10 +2074,10 @@ packages:
dependency: transitive
description:
name: sqflite_darwin
sha256: "769733dddf94622d5541c73e4ddc6aa7b252d865285914b6fcd54a63c4b4f027"
sha256: "96a698e2bc82bd770a4d6aab00b42396a7c63d9e33513a56945cbccb594c2474"
url: "https://pub.dev"
source: hosted
version: "2.4.1-1"
version: "2.4.1"
sqflite_platform_interface:
dependency: transitive
description:

View File

@@ -36,7 +36,7 @@ dependencies:
path_provider: 2.1.5
calendar_date_picker2: 1.1.7
logger: 2.4.0
flutter_drawing_board: ^0.9.5
flutter_drawing_board: 0.9.5
flutter_displaymode: 0.6.0
fl_chart: 0.69.0
file_picker: 8.1.3
@@ -44,20 +44,20 @@ dependencies:
local_auth_android: 1.0.46
permission_handler: 11.3.1
image_picker: 1.1.2
device_info_plus: 11.1.0
device_info_plus: 11.1.1
flutter_image_compress: 2.3.0
photo_view: 0.15.0
package_info_plus: 8.1.0
package_info_plus: 8.1.1
uuid: 4.5.1
flutter_quill: 10.8.5
share_plus: 10.1.1
share_plus: 10.1.2
url_launcher: 6.3.1
archive: 3.6.1
crypto: 3.0.6
markdown_widget: 2.3.2+6
flutter_colorpicker: 1.1.0
geolocator: 13.0.1
shared_preferences: 2.3.2
shared_preferences: 2.3.3
isar: 4.0.0-dev.14
isar_flutter_libs: 4.0.0-dev.14
fluttertoast: 8.2.8
@@ -91,6 +91,9 @@ dependencies:
smooth_page_indicator: 1.2.0+3
table_calendar: 3.1.2
unicons: 3.0.0
network_info_plus: 6.1.1
encrypt: 5.0.3
objectbox: 4.0.3
flutter_localizations:
@@ -129,13 +132,4 @@ flutter:
uses-material-design: true
msix_config:
display_name: 心绪日记
app_installer:
publish_folder_path: \win
publisher_display_name: ZhuJHua
# certificate_path: D:\msix.pfx
# certificate_password: liuzhuming
logo_path: assets/icon/icon.png
trim_logo: false
identity_name: cn.yooss.moodiary