From 2eb6ab7409391b6a2ac914e47582456be5061830 Mon Sep 17 00:00:00 2001 From: ZhuJHua <1624109111@qq.com> Date: Mon, 14 Apr 2025 15:30:23 +0800 Subject: [PATCH] feat: add code scanning to import support --- android/settings.gradle | 4 +- ios/Podfile.lock | 7 + lib/components/base/expand_tap_area.dart | 185 ++++++++++ lib/components/base/popup.dart | 64 ++++ lib/components/base/qr/qr_code.dart | 134 +++++++ lib/components/base/qr/qr_scanner.dart | 334 ++++++++++++++++++ lib/components/base/tile/qr_tile.dart | 169 +++++++++ .../{ => base}/tile/setting_tile.dart | 21 +- .../local_send_client_view.dart | 2 +- .../local_send_server_view.dart | 2 +- lib/components/side_bar/side_bar_view.dart | 2 +- .../web_dav_dashboard_view.dart | 2 +- lib/components/web_dav/web_dav_view.dart | 2 +- lib/l10n/app_localizations.dart | 78 ++++ lib/l10n/app_localizations_en.dart | 43 ++- lib/l10n/app_localizations_zh.dart | 41 +++ lib/l10n/intl_en.arb | 15 +- lib/l10n/intl_zh.arb | 15 +- lib/pages/about/about_logic.dart | 5 + lib/pages/about/about_view.dart | 222 ++++++------ lib/pages/backup_sync/backup_sync_view.dart | 2 +- .../category_manager_view.dart | 2 +- .../diary_setting/diary_setting_view.dart | 2 +- lib/pages/edit/edit_view.dart | 2 +- lib/pages/font/font_view.dart | 2 +- lib/pages/home/setting/setting_logic.dart | 32 +- lib/pages/home/setting/setting_state.dart | 2 +- lib/pages/home/setting/setting_view.dart | 88 +++-- lib/pages/laboratory/laboratory_logic.dart | 48 ++- lib/pages/laboratory/laboratory_view.dart | 147 +++----- lib/pages/recycle/recycle_view.dart | 2 +- lib/router/app_pages.dart | 46 +++ lib/utils/aes_util.dart | 52 +++ lib/utils/notice_util.dart | 3 + lib/utils/theme_util.dart | 22 ++ macos/Flutter/GeneratedPluginRegistrant.swift | 2 + macos/Podfile.lock | 56 ++- pubspec.lock | 48 +++ pubspec.yaml | 5 + 39 files changed, 1600 insertions(+), 310 deletions(-) create mode 100644 lib/components/base/expand_tap_area.dart create mode 100644 lib/components/base/popup.dart create mode 100644 lib/components/base/qr/qr_code.dart create mode 100644 lib/components/base/qr/qr_scanner.dart create mode 100644 lib/components/base/tile/qr_tile.dart rename lib/components/{ => base}/tile/setting_tile.dart (90%) diff --git a/android/settings.gradle b/android/settings.gradle index e985579..84e497c 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -25,8 +25,8 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version '8.9.0' apply false - id "org.jetbrains.kotlin.android" version "2.1.10" apply false + id "com.android.application" version '8.9.1' apply false + id "org.jetbrains.kotlin.android" version "2.1.20" apply false } include ":app" diff --git a/ios/Podfile.lock b/ios/Podfile.lock index b4d5e7b..aee4cbf 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -95,6 +95,9 @@ PODS: - Mantle/extobjc (2.2.0) - media_kit_video (0.0.1): - Flutter + - mobile_scanner (7.0.0): + - Flutter + - FlutterMacOS - moodiary_rust (0.0.1): - Flutter - network_info_plus (0.0.1): @@ -178,6 +181,7 @@ DEPENDENCIES: - isar_flutter_libs (from `.symlinks/plugins/isar_flutter_libs/ios`) - local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`) - media_kit_video (from `.symlinks/plugins/media_kit_video/ios`) + - mobile_scanner (from `.symlinks/plugins/mobile_scanner/darwin`) - moodiary_rust (from `.symlinks/plugins/moodiary_rust/ios`) - network_info_plus (from `.symlinks/plugins/network_info_plus/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) @@ -246,6 +250,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/local_auth_darwin/darwin" media_kit_video: :path: ".symlinks/plugins/media_kit_video/ios" + mobile_scanner: + :path: ".symlinks/plugins/mobile_scanner/darwin" moodiary_rust: :path: ".symlinks/plugins/moodiary_rust/ios" network_info_plus: @@ -301,6 +307,7 @@ SPEC CHECKSUMS: local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391 Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d media_kit_video: f3b0d035d89def15cfbbcf7dc2ae278f201e2f83 + mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93 moodiary_rust: e75b3fb63e53d3ba5cfed0edf0b6df5f98c4c5f1 network_info_plus: cf61925ab5205dce05a4f0895989afdb6aade5fc OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 diff --git a/lib/components/base/expand_tap_area.dart b/lib/components/base/expand_tap_area.dart new file mode 100644 index 0000000..381638c --- /dev/null +++ b/lib/components/base/expand_tap_area.dart @@ -0,0 +1,185 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +Color debugPaintExpandAreaColor = const Color( + 0xFFFF0000, +).withValues(alpha: 0.03); + +Color debugPaintClipAreaColor = const Color(0xFF0000FF).withValues(alpha: 0.02); + +class ExpandTapWidget extends SingleChildRenderObjectWidget { + const ExpandTapWidget({ + super.key, + super.child, + required this.onTap, + required this.tapPadding, + }); + + final VoidCallback onTap; + final EdgeInsets tapPadding; + + @override + RenderObject createRenderObject(BuildContext context) => + _ExpandTapRenderBox(onTap: onTap, tapPadding: tapPadding); + + @override + void updateRenderObject(BuildContext context, RenderBox renderObject) { + renderObject as _ExpandTapRenderBox; + if (renderObject.tapPadding != tapPadding) { + renderObject.tapPadding = tapPadding; + } + if (renderObject.onTap != onTap) { + renderObject.onTap = onTap; + } + } +} + +class _TmpGestureArenaMember extends GestureArenaMember { + _TmpGestureArenaMember({required this.onTap}); + + final VoidCallback onTap; + + @override + void acceptGesture(int key) { + onTap(); + } + + @override + void rejectGesture(int key) {} +} + +class _ExpandTapRenderBox extends RenderBox + with RenderObjectWithChildMixin { + _ExpandTapRenderBox({ + required VoidCallback onTap, + required EdgeInsets tapPadding, + }) : _onTap = onTap, + _tapPadding = tapPadding; + + VoidCallback _onTap; + EdgeInsets _tapPadding; + + set onTap(VoidCallback value) { + if (_onTap != value) { + _onTap = value; + } + } + + set tapPadding(EdgeInsets value) { + if (_tapPadding == value) return; + _tapPadding = value; + markNeedsPaint(); + } + + EdgeInsets get tapPadding => _tapPadding; + + VoidCallback get onTap => _onTap; + + @override + void performLayout() { + child!.layout(constraints, parentUsesSize: true); + size = child!.size; + if (size.isEmpty) { + _tapPadding = EdgeInsets.zero; + } + } + + @override + void paint(PaintingContext context, Offset offset) { + if (child != null) { + final BoxParentData childParentData = child!.parentData! as BoxParentData; + context.paintChild(child!, childParentData.offset + offset); + } + assert(() { + debugPaintExpandArea(context, offset); + return true; + }()); + } + + void debugPaintExpandArea(PaintingContext context, Offset offset) { + if (size.isEmpty) return; + + final RenderBox parentBox = parent as RenderBox; + + Offset parentPosition = Offset.zero; + parentPosition = offset - localToGlobal(Offset.zero, ancestor: parentBox); + + final Size parentSize = parentBox.size; + final Rect parentRect = Rect.fromLTWH( + parentPosition.dx, + parentPosition.dy, + parentSize.width, + parentSize.height, + ); + final BoxParentData childParentData = child!.parentData! as BoxParentData; + final Offset paintOffset = + childParentData.offset + offset - tapPadding.topLeft; + final Rect paintRect = Rect.fromLTWH( + paintOffset.dx, + paintOffset.dy, + size.width + tapPadding.horizontal, + size.height + tapPadding.vertical, + ); + final Paint paint = + Paint() + ..style = PaintingStyle.fill + ..strokeWidth = 1.0 + ..color = debugPaintExpandAreaColor; + + final Paint paint2 = + Paint() + ..style = PaintingStyle.fill + ..strokeWidth = 1.0 + ..color = debugPaintClipAreaColor; + context.canvas.drawRect(paintRect, paint); + context.canvas.drawRect(paintRect.intersect(parentRect), paint2); + } + + @override + void handleEvent(PointerEvent event, BoxHitTestEntry entry) { + if (event is PointerDownEvent) { + final _TmpGestureArenaMember member = _TmpGestureArenaMember( + onTap: onTap, + ); + GestureBinding.instance.gestureArena.add(event.pointer, member); + } else if (event is PointerUpEvent) { + GestureBinding.instance.gestureArena.sweep(event.pointer); + } + } + + @override + bool hitTestSelf(Offset position) => true; + + @override + bool hitTestChildren(BoxHitTestResult result, {Offset? position}) { + visitChildren((child) { + if (child is RenderBox) { + final BoxParentData parentData = child.parentData! as BoxParentData; + if (child.hitTest(result, position: position! - parentData.offset)) { + return; + } + } + }); + return false; + } + + @override + bool hitTest(BoxHitTestResult result, {Offset? position}) { + final Rect expandRect = Rect.fromLTWH( + 0 - tapPadding.left, + 0 - tapPadding.top, + size.width + tapPadding.right + tapPadding.left, + size.height + tapPadding.top + tapPadding.bottom, + ); + if (expandRect.contains(position!)) { + final bool hitTarget = + hitTestChildren(result, position: position) || hitTestSelf(position); + if (hitTarget) { + result.add(BoxHitTestEntry(this, position)); + return true; + } + } + return false; + } +} diff --git a/lib/components/base/popup.dart b/lib/components/base/popup.dart new file mode 100644 index 0000000..5a6aac0 --- /dev/null +++ b/lib/components/base/popup.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; +import 'package:moodiary/common/values/border.dart'; + +class _TrianglePainter extends CustomPainter { + final Color color; + + final Size size; + + _TrianglePainter({required this.color, required this.size}); + + @override + void paint(Canvas canvas, Size size) { + final paint = + Paint() + ..color = color + ..style = PaintingStyle.fill; + + final path = + Path() + ..moveTo(0, size.height) + ..lineTo(size.width, size.height) + ..lineTo(size.width / 2, 0) + ..close(); + + canvas.drawPath(path, paint); + } + + @override + bool shouldRepaint(covariant _TrianglePainter oldDelegate) => false; +} + +void showPopupWidget({ + required BuildContext targetContext, + required Widget child, +}) { + SmartDialog.showAttach( + targetContext: targetContext, + maskColor: Colors.transparent, + builder: (context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + CustomPaint( + size: const Size(12, 6), + painter: _TrianglePainter( + color: context.theme.colorScheme.surfaceContainer, + size: const Size(12, 6), + ), + ), + Container( + padding: const EdgeInsets.all(6.0), + decoration: BoxDecoration( + color: context.theme.colorScheme.surfaceContainer, + borderRadius: AppBorderRadius.mediumBorderRadius, + ), + child: child, + ), + ], + ); + }, + ); +} diff --git a/lib/components/base/qr/qr_code.dart b/lib/components/base/qr/qr_code.dart new file mode 100644 index 0000000..ba76600 --- /dev/null +++ b/lib/components/base/qr/qr_code.dart @@ -0,0 +1,134 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:moodiary/utils/aes_util.dart'; +import 'package:qr_flutter/qr_flutter.dart'; + +class EncryptQrCode extends StatefulWidget { + final String data; + final double size; + final Duration validDuration; + final String? prefix; + + const EncryptQrCode({ + super.key, + required this.data, + this.size = 64, + this.validDuration = const Duration(minutes: 2), + this.prefix, + }); + + @override + State createState() => _EncryptQrCodeState(); +} + +class _EncryptQrCodeState extends State { + Uint8List? encryptedData; + late int expireAt; // Unix 时间戳 + bool isExpired = false; + + @override + void initState() { + super.initState(); + _encryptData(); + } + + Future _encryptData() async { + final now = DateTime.now().millisecondsSinceEpoch ~/ 1000; + expireAt = now + widget.validDuration.inSeconds; + + final aesData = await AesUtil.encryptWithTimeWindow( + data: '${widget.prefix}${widget.data}', + validDuration: widget.validDuration, + ); + + setState(() { + isExpired = false; + encryptedData = aesData; + }); + + _startExpirationChecker(); + } + + void _startExpirationChecker() { + Timer.periodic(const Duration(seconds: 1), (timer) { + final now = DateTime.now().millisecondsSinceEpoch ~/ 1000; + if (now >= expireAt) { + setState(() { + isExpired = true; + }); + timer.cancel(); + } + }); + } + + Widget _buildQrChild() { + if (encryptedData == null) { + return Center( + key: const ValueKey('loading'), + child: CircularProgressIndicator( + color: context.theme.colorScheme.onSurfaceVariant, + ), + ); + } + + if (isExpired) { + return GestureDetector( + key: const ValueKey('expired'), + onTap: _encryptData, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.error_rounded, + color: context.theme.colorScheme.onSurfaceVariant, + ), + const SizedBox(height: 8), + Text( + '已过期', + style: context.textTheme.labelMedium?.copyWith( + color: context.theme.colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ); + } + + return QrImageView( + key: const ValueKey('qr'), + data: base64Encode(encryptedData!), + size: widget.size, + backgroundColor: Colors.transparent, + dataModuleStyle: QrDataModuleStyle( + color: context.theme.colorScheme.onSurface, + dataModuleShape: QrDataModuleShape.circle, + ), + eyeStyle: QrEyeStyle( + color: context.theme.colorScheme.onSurface, + eyeShape: QrEyeShape.circle, + ), + gapless: false, + padding: EdgeInsets.zero, + ); + } + + @override + Widget build(BuildContext context) { + return SizedBox( + width: widget.size, + height: widget.size, + child: AnimatedSwitcher( + duration: Durations.medium2, + switchInCurve: Curves.easeIn, + switchOutCurve: Curves.easeOut, + child: _buildQrChild(), + ), + ); + } +} diff --git a/lib/components/base/qr/qr_scanner.dart b/lib/components/base/qr/qr_scanner.dart new file mode 100644 index 0000000..80b288d --- /dev/null +++ b/lib/components/base/qr/qr_scanner.dart @@ -0,0 +1,334 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:dartx/dartx.dart'; +import 'package:flutter/material.dart'; +import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:moodiary/l10n/l10n.dart'; +import 'package:moodiary/router/app_pages.dart'; +import 'package:moodiary/utils/aes_util.dart'; +import 'package:moodiary/utils/log_util.dart'; +import 'package:moodiary/utils/notice_util.dart'; +import 'package:throttling/throttling.dart'; + +Future showQrScanner({ + required BuildContext context, + Duration? validDuration, + String? prefix, +}) async { + return Navigator.push( + context, + MoodiaryFadeInPageRoute( + builder: (context) { + return QrScanner(validDuration: validDuration, prefix: prefix); + }, + ), + ); +} + +class QrScanner extends StatefulWidget { + final Duration? validDuration; + + final String? prefix; + + const QrScanner({super.key, this.validDuration, this.prefix}); + + @override + State createState() => _QrScannerState(); +} + +class _QrScannerState extends State + with SingleTickerProviderStateMixin { + late final AnimationController _animationController = AnimationController( + vsync: this, + duration: Durations.long4, + )..repeat(reverse: true); + + late final Animation _curvedAnimation = CurvedAnimation( + parent: _animationController, + curve: Curves.easeInOut, + ); + + late final MobileScannerController _scannerController = + MobileScannerController( + invertImage: true, + autoStart: false, + cameraResolution: const Size.square(640), + formats: [BarcodeFormat.qrCode], + ); + + late final Throttling _throttling = Throttling(); + + late final AppLifecycleListener _appLifecycleListener; + + StreamSubscription? _subscription; + + late final ValueNotifier _torchState = ValueNotifier( + TorchState.unavailable, + ); + + @override + void initState() { + _subscription = _scannerController.barcodes.listen(_handleBarcode); + + _appLifecycleListener = AppLifecycleListener( + onStateChange: (state) { + if (!_scannerController.value.hasCameraPermission) { + return; + } + switch (state) { + case AppLifecycleState.detached: + case AppLifecycleState.hidden: + case AppLifecycleState.paused: + return; + case AppLifecycleState.resumed: + _subscription = _scannerController.barcodes.listen(_handleBarcode); + unawaited(_scannerController.start()); + case AppLifecycleState.inactive: + unawaited(_subscription?.cancel()); + _subscription = null; + unawaited(_scannerController.stop()); + } + }, + ); + _scannerController.addListener(() { + if (_scannerController.value.torchState != _torchState.value) { + _torchState.value = _scannerController.value.torchState; + } + }); + unawaited(_scannerController.start()); + super.initState(); + } + + @override + void dispose() async { + unawaited(_subscription?.cancel()); + _subscription = null; + _animationController.dispose(); + _throttling.close(); + _appLifecycleListener.dispose(); + super.dispose(); + await _scannerController.dispose(); + } + + void _handleBarcode(BarcodeCapture value) { + _throttling.throttle(() async { + final raw = value.barcodes.firstOrNull?.rawValue; + if (raw == null) return; + + try { + if (widget.validDuration != null) { + final resBytes = base64Decode(raw); + final decrypted = await AesUtil.decryptWithTimeWindow( + encryptedData: resBytes, + validDuration: widget.validDuration!, + ); + if (decrypted.isNullOrBlank) { + _handleInvalidQr(); + return; + } + if (widget.prefix.isNotNullOrBlank) { + if (!decrypted!.startsWith(widget.prefix!)) { + _handleInvalidQr(); + return; + } + final realData = decrypted.substring(widget.prefix!.length); + if (mounted) Navigator.pop(context, realData); + } else { + if (mounted) Navigator.pop(context, decrypted); + } + } else { + if (raw.isNullOrBlank) { + _handleInvalidQr(); + } else { + if (mounted) Navigator.pop(context, raw); + } + } + } catch (e) { + logger.d(e); + _handleInvalidQr(); + } + }); + } + + void _handleInvalidQr() { + if (mounted) { + toast.info(message: context.l10n.qrCodeInvalid); + } + } + + @override + Widget build(BuildContext context) { + final scanWindow = Rect.fromCenter( + center: MediaQuery.sizeOf(context).center(Offset.zero), + width: 200, + height: 200, + ); + + return Scaffold( + appBar: AppBar( + backgroundColor: Colors.transparent, + leading: BackButton( + color: Colors.white, + onPressed: () { + Navigator.pop(context); + }, + ), + ), + extendBodyBehindAppBar: true, + body: Stack( + alignment: Alignment.center, + children: [ + MobileScanner( + scanWindow: scanWindow, + controller: _scannerController, + overlayBuilder: (context, constraints) { + return AnimatedBuilder( + animation: _curvedAnimation, + builder: (context, child) { + return Stack( + children: [ + CustomPaint( + size: constraints.biggest, + painter: _ScannerOverlayPainter( + centerSize: _curvedAnimation.value * 20 + 200, + ), + ), + Center( + child: CustomPaint( + size: Size.square(_curvedAnimation.value * 20 + 200), + painter: _CornerBorderPainter( + color: Colors.white, + strokeWidth: 4, + cornerLength: _curvedAnimation.value * 2 + 20, + radius: 12, + ), + ), + ), + ], + ); + }, + ); + }, + ), + Positioned( + bottom: MediaQuery.sizeOf(context).height / 2 - 300, + child: ValueListenableBuilder( + valueListenable: _torchState, + builder: (context, value, child) { + if (value == TorchState.unavailable) { + return const SizedBox.shrink(); + } + return IconButton( + onPressed: () async { + await _scannerController.toggleTorch(); + }, + iconSize: 56, + icon: switch (_torchState.value) { + TorchState.auto => const Icon( + Icons.flash_auto_rounded, + color: Colors.white, + ), + TorchState.off => const Icon( + Icons.flashlight_off_rounded, + color: Colors.white54, + ), + TorchState.on => const Icon( + Icons.flashlight_on_rounded, + color: Colors.white, + ), + TorchState.unavailable => throw UnimplementedError(), + }, + ); + }, + ), + ), + ], + ), + ); + } +} + +class _CornerBorderPainter extends CustomPainter { + final Color color; + final double strokeWidth; + final double cornerLength; + final double radius; + + _CornerBorderPainter({ + required this.color, + this.strokeWidth = 2, + this.cornerLength = 20, + this.radius = 0, + }); + + @override + void paint(Canvas canvas, Size size) { + final paint = + Paint() + ..color = color + ..strokeWidth = strokeWidth + ..style = PaintingStyle.stroke + ..strokeCap = StrokeCap.round; + + final path = Path(); + + final double w = size.width; + final double h = size.height; + final r = radius; + + path.moveTo(0, r + cornerLength); + path.lineTo(0, r); + path.quadraticBezierTo(0, 0, r, 0); + path.lineTo(r + cornerLength, 0); + + path.moveTo(w - r - cornerLength, 0); + path.lineTo(w - r, 0); + path.quadraticBezierTo(w, 0, w, r); + path.lineTo(w, r + cornerLength); + + path.moveTo(w, h - r - cornerLength); + path.lineTo(w, h - r); + path.quadraticBezierTo(w, h, w - r, h); + path.lineTo(w - r - cornerLength, h); + + path.moveTo(r + cornerLength, h); + path.lineTo(r, h); + path.quadraticBezierTo(0, h, 0, h - r); + path.lineTo(0, h - r - cornerLength); + + canvas.drawPath(path, paint); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} + +class _ScannerOverlayPainter extends CustomPainter { + final double centerSize; + + _ScannerOverlayPainter({required this.centerSize}); + + @override + void paint(Canvas canvas, Size size) { + final paint = + Paint() + ..color = Colors.black54 + ..style = PaintingStyle.fill; + + final outer = Path()..addRect(Rect.fromLTWH(0, 0, size.width, size.height)); + + final scanSize = centerSize; + final left = (size.width - scanSize) / 2; + final top = (size.height - scanSize) / 2; + final scanRect = Rect.fromLTWH(left, top, scanSize, scanSize); + + final hole = Path()..addRRect(RRect.fromRectXY(scanRect, 12, 12)); + final overlay = Path.combine(PathOperation.difference, outer, hole); + + canvas.drawPath(overlay, paint); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} diff --git a/lib/components/base/tile/qr_tile.dart b/lib/components/base/tile/qr_tile.dart new file mode 100644 index 0000000..38810d7 --- /dev/null +++ b/lib/components/base/tile/qr_tile.dart @@ -0,0 +1,169 @@ +import 'package:adaptive_dialog/adaptive_dialog.dart'; +import 'package:dartx/dartx.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:moodiary/common/values/border.dart'; +import 'package:moodiary/components/base/popup.dart'; +import 'package:moodiary/components/base/qr/qr_code.dart'; +import 'package:moodiary/components/base/qr/qr_scanner.dart'; +import 'package:moodiary/components/base/tile/setting_tile.dart'; +import 'package:moodiary/l10n/l10n.dart'; +import 'package:moodiary/utils/notice_util.dart'; + +class QrInputTile extends StatelessWidget { + final String title; + final String? subtitle; + + final String? prefix; + + final String value; + final bool withStyle; + + final void Function(String)? onValue; + + final Widget? leading; + + final VoidCallback? onScan; + final VoidCallback? onInput; + + const QrInputTile({ + super.key, + required this.title, + this.subtitle, + required this.value, + this.onValue, + this.withStyle = true, + this.onScan, + this.onInput, + this.leading, + this.prefix, + }); + + @override + Widget build(BuildContext context) { + const validDuration = Duration(minutes: 2); + return AdaptiveListTile( + title: Text(title), + leading: leading, + subtitle: + subtitle ?? + (value.isNotNullOrBlank + ? Text(context.l10n.hasOption) + : Text(context.l10n.noOption)), + tileColor: + withStyle ? context.theme.colorScheme.surfaceContainerLow : null, + shape: + withStyle + ? const RoundedRectangleBorder( + borderRadius: AppBorderRadius.mediumBorderRadius, + ) + : null, + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Builder( + builder: (context) { + return IconButton.filled( + tooltip: context.l10n.genQrCodeTooltip, + onPressed: () { + if (value.isBlank) { + toast.info(message: context.l10n.genQrCodeError1(title)); + return; + } + showPopupWidget( + targetContext: context, + child: EncryptQrCode( + data: value, + size: 96, + prefix: prefix, + validDuration: validDuration, + ), + ); + }, + icon: Icon( + Icons.qr_code_rounded, + color: context.theme.colorScheme.onPrimary, + ), + ); + }, + ), + IconButton.filled( + tooltip: context.l10n.inputTooltip, + onPressed: () async { + final choice = await showDialog( + context: context, + builder: (context) { + return SimpleDialog( + title: Text(context.l10n.inputMethodTitle), + children: [ + SimpleDialogOption( + child: Row( + spacing: 8.0, + children: [ + const Icon(Icons.qr_code_scanner_rounded), + Text(context.l10n.inputMethodScanQrCode), + ], + ), + onPressed: () { + Navigator.pop(context, 'qr'); + }, + ), + SimpleDialogOption( + child: Row( + spacing: 8.0, + children: [ + const Icon(Icons.keyboard_rounded), + Text(context.l10n.inputMethodHandelInput), + ], + ), + onPressed: () { + Navigator.pop(context, 'input'); + }, + ), + ], + ); + }, + ); + if (choice == null) return; + if (choice == 'qr' && context.mounted) { + if (onScan != null) { + onScan?.call(); + return; + } + final res = await showQrScanner( + context: context, + validDuration: validDuration, + prefix: prefix, + ); + if (res != null) { + onValue?.call(res); + } + } + if (choice == 'input' && context.mounted) { + if (onInput != null) { + onInput?.call(); + return; + } + final res = await showTextInputDialog( + context: context, + textFields: [DialogTextField(initialText: value)], + title: title, + message: context.l10n.getKeyFromConsole, + style: AdaptiveStyle.material, + ); + if (res != null && res.isNotEmpty) { + final value = res[0]; + onValue?.call(value); + } + } + }, + icon: Icon( + Icons.input_rounded, + color: context.theme.colorScheme.onPrimary, + ), + ), + ], + ), + ); + } +} diff --git a/lib/components/tile/setting_tile.dart b/lib/components/base/tile/setting_tile.dart similarity index 90% rename from lib/components/tile/setting_tile.dart rename to lib/components/base/tile/setting_tile.dart index 9d8a92e..c2bddf3 100644 --- a/lib/components/tile/setting_tile.dart +++ b/lib/components/base/tile/setting_tile.dart @@ -40,6 +40,8 @@ class AdaptiveListTile extends StatelessWidget { this.isFirst, this.isLast, this.contentPadding, + this.tileColor, + this.shape, }); final dynamic title; @@ -58,6 +60,10 @@ class AdaptiveListTile extends StatelessWidget { final EdgeInsets? contentPadding; + final Color? tileColor; + + final ShapeBorder? shape; + @override Widget build(BuildContext context) { assert( @@ -77,16 +83,19 @@ class AdaptiveListTile extends StatelessWidget { realSubtitle = (subtitle as Text).data; } return ListTile( + tileColor: tileColor, title: (realTitle is String) ? AdaptiveText(realTitle, isTileTitle: true) : realTitle, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: isFirst == true ? const Radius.circular(12) : Radius.zero, - bottom: isLast == true ? const Radius.circular(12) : Radius.zero, - ), - ), + shape: + shape ?? + RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: isFirst == true ? const Radius.circular(12) : Radius.zero, + bottom: isLast == true ? const Radius.circular(12) : Radius.zero, + ), + ), contentPadding: contentPadding, subtitle: (realSubtitle is String) diff --git a/lib/components/local_send/local_send_client/local_send_client_view.dart b/lib/components/local_send/local_send_client/local_send_client_view.dart index e755181..3c854d4 100644 --- a/lib/components/local_send/local_send_client/local_send_client_view.dart +++ b/lib/components/local_send/local_send_client/local_send_client_view.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; -import 'package:moodiary/components/tile/setting_tile.dart'; +import 'package:moodiary/components/base/tile/setting_tile.dart'; import 'package:moodiary/l10n/l10n.dart'; import 'package:moodiary/utils/file_util.dart'; diff --git a/lib/components/local_send/local_send_server/local_send_server_view.dart b/lib/components/local_send/local_send_server/local_send_server_view.dart index 6919d39..207155e 100644 --- a/lib/components/local_send/local_send_server/local_send_server_view.dart +++ b/lib/components/local_send/local_send_server/local_send_server_view.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; -import 'package:moodiary/components/tile/setting_tile.dart'; +import 'package:moodiary/components/base/tile/setting_tile.dart'; import 'package:moodiary/l10n/l10n.dart'; import 'local_send_server_logic.dart'; diff --git a/lib/components/side_bar/side_bar_view.dart b/lib/components/side_bar/side_bar_view.dart index 26f38e8..87b45fa 100644 --- a/lib/components/side_bar/side_bar_view.dart +++ b/lib/components/side_bar/side_bar_view.dart @@ -2,7 +2,7 @@ // import 'package:flutter/material.dart'; // import 'package:intl/intl.dart'; // import 'package:moodiary/common/values/icons.dart'; -// import 'package:moodiary/components/tile/setting_tile.dart'; +// import 'package:moodiary/components/base/tile/setting_tile.dart'; // import 'package:moodiary/main.dart'; // import 'package:moodiary/utils/function_extensions.dart'; // import 'package:get/get.dart'; diff --git a/lib/components/sync_dash_board/web_dav_dashboard/web_dav_dashboard_view.dart b/lib/components/sync_dash_board/web_dav_dashboard/web_dav_dashboard_view.dart index 6f87529..ab48b6b 100644 --- a/lib/components/sync_dash_board/web_dav_dashboard/web_dav_dashboard_view.dart +++ b/lib/components/sync_dash_board/web_dav_dashboard/web_dav_dashboard_view.dart @@ -3,7 +3,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; import 'package:moodiary/common/values/webdav.dart'; import 'package:moodiary/components/base/loading.dart'; -import 'package:moodiary/components/tile/setting_tile.dart'; +import 'package:moodiary/components/base/tile/setting_tile.dart'; import 'package:moodiary/l10n/l10n.dart'; import 'package:moodiary/utils/webdav_util.dart'; diff --git a/lib/components/web_dav/web_dav_view.dart b/lib/components/web_dav/web_dav_view.dart index 9ba34fb..2250c3e 100644 --- a/lib/components/web_dav/web_dav_view.dart +++ b/lib/components/web_dav/web_dav_view.dart @@ -3,7 +3,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; import 'package:moodiary/common/values/border.dart'; import 'package:moodiary/common/values/webdav.dart'; -import 'package:moodiary/components/tile/setting_tile.dart'; +import 'package:moodiary/components/base/tile/setting_tile.dart'; import 'package:moodiary/l10n/l10n.dart'; import 'web_dav_logic.dart'; diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 5a48ce1..9e70cdc 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1753,6 +1753,84 @@ abstract class AppLocalizations { /// In zh, this message translates to: /// **'加载中'** String get toastLoading; + + /// No description provided for @genQrCodeError1. + /// + /// In zh, this message translates to: + /// **'请先配置 {name}'** + String genQrCodeError1(Object name); + + /// No description provided for @genQrCodeTooltip. + /// + /// In zh, this message translates to: + /// **'生成二维码'** + String get genQrCodeTooltip; + + /// No description provided for @qrCodeInvalid. + /// + /// In zh, this message translates to: + /// **'二维码无效'** + String get qrCodeInvalid; + + /// No description provided for @inputTooltip. + /// + /// In zh, this message translates to: + /// **'输入'** + String get inputTooltip; + + /// No description provided for @inputMethodTitle. + /// + /// In zh, this message translates to: + /// **'输入方式'** + String get inputMethodTitle; + + /// No description provided for @inputMethodScanQrCode. + /// + /// In zh, this message translates to: + /// **'扫描二维码'** + String get inputMethodScanQrCode; + + /// No description provided for @inputMethodHandelInput. + /// + /// In zh, this message translates to: + /// **'手动输入'** + String get inputMethodHandelInput; + + /// No description provided for @getKeyFromConsole. + /// + /// In zh, this message translates to: + /// **'请从对应控制台获取密钥'** + String get getKeyFromConsole; + + /// No description provided for @hasOption. + /// + /// In zh, this message translates to: + /// **'已配置'** + String get hasOption; + + /// No description provided for @noOption. + /// + /// In zh, this message translates to: + /// **'未配置'** + String get noOption; + + /// No description provided for @labQweather. + /// + /// In zh, this message translates to: + /// **'和风天气'** + String get labQweather; + + /// No description provided for @labTianditu. + /// + /// In zh, this message translates to: + /// **'天地图'** + String get labTianditu; + + /// No description provided for @labTencentCloud. + /// + /// In zh, this message translates to: + /// **'腾讯云'** + String get labTencentCloud; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index b4b9f4f..cc53b5b 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1,6 +1,5 @@ // ignore: unused_import import 'package:intl/intl.dart' as intl; - import 'app_localizations.dart'; // ignore_for_file: type=lint @@ -885,4 +884,46 @@ class AppLocalizationsEn extends AppLocalizations { @override String get toastLoading => 'Loading'; + + @override + String genQrCodeError1(Object name) { + return 'Please configure $name first'; + } + + @override + String get genQrCodeTooltip => 'Generate QR code'; + + @override + String get qrCodeInvalid => 'QR code invalid'; + + @override + String get inputTooltip => 'Input'; + + @override + String get inputMethodTitle => 'Input method'; + + @override + String get inputMethodScanQrCode => 'Scan QR code'; + + @override + String get inputMethodHandelInput => 'Manual input'; + + @override + String get getKeyFromConsole => + 'Please get the key from the corresponding console'; + + @override + String get hasOption => 'Configured'; + + @override + String get noOption => 'Not configured'; + + @override + String get labQweather => 'Qweather'; + + @override + String get labTianditu => 'Tianditu'; + + @override + String get labTencentCloud => 'Tencent Cloud'; } diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 05b2027..ffdb6a9 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -852,4 +852,45 @@ class AppLocalizationsZh extends AppLocalizations { @override String get toastLoading => '加载中'; + + @override + String genQrCodeError1(Object name) { + return '请先配置 $name'; + } + + @override + String get genQrCodeTooltip => '生成二维码'; + + @override + String get qrCodeInvalid => '二维码无效'; + + @override + String get inputTooltip => '输入'; + + @override + String get inputMethodTitle => '输入方式'; + + @override + String get inputMethodScanQrCode => '扫描二维码'; + + @override + String get inputMethodHandelInput => '手动输入'; + + @override + String get getKeyFromConsole => '请从对应控制台获取密钥'; + + @override + String get hasOption => '已配置'; + + @override + String get noOption => '未配置'; + + @override + String get labQweather => '和风天气'; + + @override + String get labTianditu => '天地图'; + + @override + String get labTencentCloud => '腾讯云'; } diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 5811d51..ebe0543 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -275,5 +275,18 @@ "mediaVideoCount": "{count, plural, =1 {# Video} other {# Videos}}", "toastSuccess": "Success", "toastError": "Error", - "toastLoading": "Loading" + "toastLoading": "Loading", + "genQrCodeError1": "Please configure {name} first", + "genQrCodeTooltip": "Generate QR code", + "qrCodeInvalid": "QR code invalid", + "inputTooltip": "Input", + "inputMethodTitle": "Input method", + "inputMethodScanQrCode": "Scan QR code", + "inputMethodHandelInput": "Manual input", + "getKeyFromConsole": "Please get the key from the corresponding console", + "hasOption": "Configured", + "noOption": "Not configured", + "labQweather": "Qweather", + "labTianditu": "Tianditu", + "labTencentCloud": "Tencent Cloud" } \ No newline at end of file diff --git a/lib/l10n/intl_zh.arb b/lib/l10n/intl_zh.arb index e2c7b07..db57e3a 100644 --- a/lib/l10n/intl_zh.arb +++ b/lib/l10n/intl_zh.arb @@ -275,5 +275,18 @@ "mediaVideoCount": "{count} 段视频", "toastSuccess": "成功", "toastError": "出错了", - "toastLoading": "加载中" + "toastLoading": "加载中", + "genQrCodeError1": "请先配置 {name}", + "genQrCodeTooltip": "生成二维码", + "qrCodeInvalid": "二维码无效", + "inputTooltip": "输入", + "inputMethodTitle": "输入方式", + "inputMethodScanQrCode": "扫描二维码", + "inputMethodHandelInput": "手动输入", + "getKeyFromConsole": "请从对应控制台获取密钥", + "hasOption": "已配置", + "noOption": "未配置", + "labQweather": "和风天气", + "labTianditu": "天地图", + "labTencentCloud": "腾讯云" } \ No newline at end of file diff --git a/lib/pages/about/about_logic.dart b/lib/pages/about/about_logic.dart index da5fbd6..936f954 100644 --- a/lib/pages/about/about_logic.dart +++ b/lib/pages/about/about_logic.dart @@ -90,4 +90,9 @@ class AboutLogic extends GetxController with GetSingleTickerProviderStateMixin { void toSponsor() { Get.toNamed(AppRoutes.sponsorPage); } + + Future toIcp() async { + final uri = Uri.parse('https://beian.miit.gov.cn/'); + await launchUrl(uri, mode: LaunchMode.platformDefault); + } } diff --git a/lib/pages/about/about_view.dart b/lib/pages/about/about_view.dart index f1378b8..c338ef7 100644 --- a/lib/pages/about/about_view.dart +++ b/lib/pages/about/about_view.dart @@ -5,8 +5,9 @@ import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; import 'package:moodiary/components/base/button.dart'; +import 'package:moodiary/components/base/expand_tap_area.dart'; import 'package:moodiary/components/base/text.dart'; -import 'package:moodiary/components/tile/setting_tile.dart'; +import 'package:moodiary/components/base/tile/setting_tile.dart'; import 'package:moodiary/gen/assets.gen.dart'; import 'package:moodiary/l10n/l10n.dart'; import 'package:moodiary/utils/update_util.dart'; @@ -91,6 +92,57 @@ class AboutPage extends StatelessWidget { ); } + Widget buildInfo() { + return Column( + spacing: 16.0, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 4.0, + children: [ + const FaIcon( + FontAwesomeIcons.flutter, + size: 16, + color: Colors.lightBlue, + ), + const SizedBox(height: 12, child: VerticalDivider(thickness: 2)), + FaIcon( + FontAwesomeIcons.dartLang, + size: 16, + color: context.theme.colorScheme.onSurface.withValues( + alpha: 0.8, + ), + ), + const SizedBox(height: 12, child: VerticalDivider(thickness: 2)), + FaIcon( + FontAwesomeIcons.rust, + size: 16, + color: context.theme.colorScheme.onSurface.withValues( + alpha: 0.8, + ), + ), + const SizedBox(height: 12, child: VerticalDivider(thickness: 2)), + const FaIcon( + FontAwesomeIcons.solidHeart, + size: 16, + color: Colors.pinkAccent, + ), + ], + ), + ExpandTapWidget( + tapPadding: const EdgeInsets.all(4.0), + onTap: logic.toIcp, + child: Text( + '赣ICP备2022010939号-4A', + style: context.textTheme.labelMedium?.copyWith( + color: context.theme.colorScheme.onSurfaceVariant, + ), + ), + ), + ], + ); + } + return Stack( children: [ Scaffold( @@ -98,114 +150,76 @@ class AboutPage extends StatelessWidget { title: Text(context.l10n.aboutTitle), leading: const PageBackButton(), ), - body: ListView( - padding: const EdgeInsets.all(16.0), - physics: const ClampingScrollPhysics(), - children: [ - buildLogoTitle(), - const SizedBox(height: 16.0), - Card.outlined( - color: context.theme.colorScheme.surfaceContainerLow, - child: Column( - children: [ - AdaptiveListTile( - leading: const Icon(Icons.update_rounded), - title: Text(context.l10n.aboutUpdate), - isFirst: true, - trailing: const Icon(Icons.chevron_right_rounded), - onTap: () async { - await UpdateUtil.checkShouldUpdate( - state.appVersion, - handle: true, - ); - }, - ), - AdaptiveListTile( - leading: const Icon(Icons.source_rounded), - title: Text(context.l10n.aboutSource), - trailing: const Icon(Icons.chevron_right_rounded), - onTap: () async { - await logic.toSource(); - }, - ), - AdaptiveListTile( - leading: const Icon(Icons.file_copy_rounded), - title: Text(context.l10n.aboutUserAgreement), - trailing: const Icon(Icons.chevron_right_rounded), - onTap: () { - logic.toAgreement(); - }, - ), - AdaptiveListTile( - leading: const Icon(Icons.privacy_tip_rounded), - title: Text(context.l10n.aboutPrivacyPolicy), - trailing: const Icon(Icons.chevron_right_rounded), - onTap: () { - logic.toPrivacy(); - }, - ), - AdaptiveListTile( - leading: const Icon(Icons.bug_report_rounded), - title: Text(context.l10n.aboutBugReport), - trailing: const Icon(Icons.chevron_right_rounded), - onTap: () async { - await logic.toReportPage(); - }, - ), - AdaptiveListTile( - leading: const Icon(Icons.attach_money_rounded), - title: Text(context.l10n.aboutDonate), - isLast: true, - trailing: const Icon(Icons.chevron_right_rounded), - onTap: logic.toSponsor, - ), - ], - ), - ), - const SizedBox(height: 16.0), - Row( - mainAxisAlignment: MainAxisAlignment.center, - spacing: 4.0, + body: SingleChildScrollView( + scrollDirection: Axis.vertical, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + spacing: 32.0, children: [ - const FaIcon( - FontAwesomeIcons.flutter, - size: 16, - color: Colors.lightBlue, - ), - const SizedBox( - height: 12, - child: VerticalDivider(thickness: 2), - ), - FaIcon( - FontAwesomeIcons.dartLang, - size: 16, - color: context.theme.colorScheme.onSurface.withValues( - alpha: 0.8, + buildLogoTitle(), + Card.outlined( + color: context.theme.colorScheme.surfaceContainerLow, + child: Column( + children: [ + AdaptiveListTile( + leading: const Icon(Icons.update_rounded), + title: Text(context.l10n.aboutUpdate), + isFirst: true, + trailing: const Icon(Icons.chevron_right_rounded), + onTap: () async { + await UpdateUtil.checkShouldUpdate( + state.appVersion, + handle: true, + ); + }, + ), + AdaptiveListTile( + leading: const Icon(Icons.source_rounded), + title: Text(context.l10n.aboutSource), + trailing: const Icon(Icons.chevron_right_rounded), + onTap: () async { + await logic.toSource(); + }, + ), + AdaptiveListTile( + leading: const Icon(Icons.file_copy_rounded), + title: Text(context.l10n.aboutUserAgreement), + trailing: const Icon(Icons.chevron_right_rounded), + onTap: () { + logic.toAgreement(); + }, + ), + AdaptiveListTile( + leading: const Icon(Icons.privacy_tip_rounded), + title: Text(context.l10n.aboutPrivacyPolicy), + trailing: const Icon(Icons.chevron_right_rounded), + onTap: () { + logic.toPrivacy(); + }, + ), + AdaptiveListTile( + leading: const Icon(Icons.bug_report_rounded), + title: Text(context.l10n.aboutBugReport), + trailing: const Icon(Icons.chevron_right_rounded), + onTap: () async { + await logic.toReportPage(); + }, + ), + AdaptiveListTile( + leading: const Icon(Icons.attach_money_rounded), + title: Text(context.l10n.aboutDonate), + isLast: true, + trailing: const Icon(Icons.chevron_right_rounded), + onTap: logic.toSponsor, + ), + ], ), ), - const SizedBox( - height: 12, - child: VerticalDivider(thickness: 2), - ), - FaIcon( - FontAwesomeIcons.rust, - size: 16, - color: context.theme.colorScheme.onSurface.withValues( - alpha: 0.8, - ), - ), - const SizedBox( - height: 12, - child: VerticalDivider(thickness: 2), - ), - const FaIcon( - FontAwesomeIcons.solidHeart, - size: 16, - color: Colors.pinkAccent, - ), + buildInfo(), ], ), - ], + ), ), ), Align( diff --git a/lib/pages/backup_sync/backup_sync_view.dart b/lib/pages/backup_sync/backup_sync_view.dart index 25de0cd..3e2b1c1 100644 --- a/lib/pages/backup_sync/backup_sync_view.dart +++ b/lib/pages/backup_sync/backup_sync_view.dart @@ -2,8 +2,8 @@ import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:moodiary/components/base/button.dart'; +import 'package:moodiary/components/base/tile/setting_tile.dart'; import 'package:moodiary/components/local_send/local_send_view.dart'; -import 'package:moodiary/components/tile/setting_tile.dart'; import 'package:moodiary/components/web_dav/web_dav_view.dart'; import 'package:moodiary/l10n/l10n.dart'; diff --git a/lib/pages/category_manager/category_manager_view.dart b/lib/pages/category_manager/category_manager_view.dart index 4ae0ab4..4265c63 100644 --- a/lib/pages/category_manager/category_manager_view.dart +++ b/lib/pages/category_manager/category_manager_view.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:moodiary/components/base/button.dart'; import 'package:moodiary/components/base/loading.dart'; -import 'package:moodiary/components/tile/setting_tile.dart'; +import 'package:moodiary/components/base/tile/setting_tile.dart'; import 'package:moodiary/l10n/l10n.dart'; import 'category_manager_logic.dart'; diff --git a/lib/pages/diary_setting/diary_setting_view.dart b/lib/pages/diary_setting/diary_setting_view.dart index a30a91a..b74eed8 100644 --- a/lib/pages/diary_setting/diary_setting_view.dart +++ b/lib/pages/diary_setting/diary_setting_view.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:moodiary/components/base/button.dart'; import 'package:moodiary/components/base/clipper.dart'; -import 'package:moodiary/components/tile/setting_tile.dart'; +import 'package:moodiary/components/base/tile/setting_tile.dart'; import 'package:moodiary/l10n/l10n.dart'; import 'diary_setting_logic.dart'; diff --git a/lib/pages/edit/edit_view.dart b/lib/pages/edit/edit_view.dart index 75d3cca..24fd622 100644 --- a/lib/pages/edit/edit_view.dart +++ b/lib/pages/edit/edit_view.dart @@ -15,6 +15,7 @@ import 'package:moodiary/common/values/colors.dart'; import 'package:moodiary/common/values/diary_type.dart'; import 'package:moodiary/components/base/button.dart'; import 'package:moodiary/components/base/sheet.dart'; +import 'package:moodiary/components/base/tile/setting_tile.dart'; import 'package:moodiary/components/category_add/category_add_view.dart'; import 'package:moodiary/components/expand_button/expand_button_view.dart'; import 'package:moodiary/components/lottie_modal/lottie_modal.dart'; @@ -26,7 +27,6 @@ import 'package:moodiary/components/quill_embed/image_embed.dart'; import 'package:moodiary/components/quill_embed/text_indent.dart'; import 'package:moodiary/components/quill_embed/video_embed.dart'; import 'package:moodiary/components/record_sheet/record_sheet_view.dart'; -import 'package:moodiary/components/tile/setting_tile.dart'; import 'package:moodiary/l10n/l10n.dart'; import 'package:moodiary/utils/theme_util.dart'; diff --git a/lib/pages/font/font_view.dart b/lib/pages/font/font_view.dart index 3c59b54..3e3be6c 100644 --- a/lib/pages/font/font_view.dart +++ b/lib/pages/font/font_view.dart @@ -6,7 +6,7 @@ import 'package:moodiary/common/values/border.dart'; import 'package:moodiary/components/base/button.dart'; import 'package:moodiary/components/base/loading.dart'; import 'package:moodiary/components/base/text.dart'; -import 'package:moodiary/components/tile/setting_tile.dart'; +import 'package:moodiary/components/base/tile/setting_tile.dart'; import 'package:moodiary/l10n/l10n.dart'; import 'font_logic.dart'; diff --git a/lib/pages/home/setting/setting_logic.dart b/lib/pages/home/setting/setting_logic.dart index e6620c0..a3cb7ea 100644 --- a/lib/pages/home/setting/setting_logic.dart +++ b/lib/pages/home/setting/setting_logic.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:dartx/dartx.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:moodiary/components/dashboard/dashboard_logic.dart'; @@ -26,13 +25,12 @@ class SettingLogic extends GetxController { @override void onReady() { unawaited(getDataUsage()); - unawaited(checkHasUserKey()); + unawaited(checkUserKey()); super.onReady(); } - Future checkHasUserKey() async { - state.hasUserKey.value = - (await SecureStorageUtil.getValue('userKey')) != null; + Future checkUserKey() async { + state.userKey.value = (await SecureStorageUtil.getValue('userKey')) ?? ''; } //获取当前占用储存空间 @@ -135,17 +133,23 @@ class SettingLogic extends GetxController { Bind.find().updateTitle(); } - Future setUserKey({required String key}) async { - if (key.isNullOrBlank) { - toast.info(message: '密钥不能为空'); - return; + Future setUserKey({required String key}) async { + try { + await SecureStorageUtil.setValue('userKey', key); + state.userKey.value = key; + return true; + } catch (e) { + return false; } - await SecureStorageUtil.setValue('userKey', key); - state.hasUserKey.value = true; } - Future removeUserKey() async { - await SecureStorageUtil.remove('userKey'); - state.hasUserKey.value = false; + Future removeUserKey() async { + try { + await SecureStorageUtil.remove('userKey'); + state.userKey.value = ''; + return true; + } catch (e) { + return false; + } } } diff --git a/lib/pages/home/setting/setting_state.dart b/lib/pages/home/setting/setting_state.dart index d16c314..2c7a0de 100644 --- a/lib/pages/home/setting/setting_state.dart +++ b/lib/pages/home/setting/setting_state.dart @@ -22,7 +22,7 @@ class SettingState { RxBool backendPrivacy = PrefUtil.getValue('backendPrivacy')!.obs; - RxBool hasUserKey = false.obs; + RxString userKey = ''.obs; Rx language = Language.values diff --git a/lib/pages/home/setting/setting_view.dart b/lib/pages/home/setting/setting_view.dart index aa6c2e5..fb43735 100644 --- a/lib/pages/home/setting/setting_view.dart +++ b/lib/pages/home/setting/setting_view.dart @@ -1,4 +1,5 @@ import 'package:adaptive_dialog/adaptive_dialog.dart'; +import 'package:dartx/dartx.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; @@ -8,14 +9,16 @@ import 'package:moodiary/common/values/language.dart'; import 'package:moodiary/components/base/clipper.dart'; import 'package:moodiary/components/base/sheet.dart'; import 'package:moodiary/components/base/text.dart'; +import 'package:moodiary/components/base/tile/qr_tile.dart'; +import 'package:moodiary/components/base/tile/setting_tile.dart'; import 'package:moodiary/components/color_sheet/color_sheet_view.dart'; import 'package:moodiary/components/dashboard/dashboard_view.dart'; import 'package:moodiary/components/language_dialog/language_dialog_view.dart'; import 'package:moodiary/components/remove_password/remove_password_view.dart'; import 'package:moodiary/components/set_password/set_password_view.dart'; import 'package:moodiary/components/theme_mode_dialog/theme_mode_dialog_view.dart'; -import 'package:moodiary/components/tile/setting_tile.dart'; import 'package:moodiary/l10n/l10n.dart'; +import 'package:moodiary/utils/notice_util.dart'; import 'setting_logic.dart'; @@ -339,44 +342,55 @@ class SettingPage extends StatelessWidget { ); }, ), - AdaptiveListTile( - title: context.l10n.settingUserKey, - subtitle: context.l10n.settingUserKeyDes, - onTap: () async { - if (state.hasUserKey.value) { - final res = await showOkCancelAlertDialog( - context: context, - title: context.l10n.settingUserKeyReset, - message: context.l10n.settingUserKeyResetDes, - ); - if (res == OkCancelResult.ok) { - logic.removeUserKey(); + Obx(() { + return QrInputTile( + title: context.l10n.settingUserKey, + value: state.userKey.value, + prefix: 'userKey', + onValue: (value) async { + final res = await logic.setUserKey(key: value); + if (res) { + toast.success(); + } else { + toast.error(); } - return; - } else { - final res = await showTextInputDialog( - title: context.l10n.settingUserKeySet, - message: context.l10n.settingUserKeySetDes, - context: context, - textFields: [const DialogTextField()], - ); - if (res != null) { - logic.setUserKey(key: res.first); + }, + onInput: () async { + if (state.userKey.value.isNotNullOrBlank) { + final res = await showOkCancelAlertDialog( + context: context, + title: context.l10n.settingUserKeyReset, + message: context.l10n.settingUserKeyResetDes, + ); + if (res == OkCancelResult.ok) { + final res_ = await logic.removeUserKey(); + if (res_) { + toast.success(); + } else { + toast.error(); + } + } + return; + } else { + final res = await showTextInputDialog( + title: context.l10n.settingUserKeySet, + message: context.l10n.settingUserKeySetDes, + context: context, + textFields: [const DialogTextField()], + ); + if (res != null) { + final res_ = await logic.setUserKey(key: res.first); + if (res_) { + toast.success(); + } else { + toast.error(); + } + } } - } - }, - trailing: Obx(() { - return Text( - state.hasUserKey.value - ? context.l10n.settingUserKeyHasSet - : context.l10n.settingUserKeyNotSet, - style: context.textTheme.bodySmall!.copyWith( - color: context.theme.colorScheme.primary, - ), - ); - }), - leading: const Icon(Icons.key_rounded), - ), + }, + leading: const Icon(Icons.key_rounded), + ); + }), GetBuilder( id: 'Lock', builder: (_) { diff --git a/lib/pages/laboratory/laboratory_logic.dart b/lib/pages/laboratory/laboratory_logic.dart index 703e11a..dced254 100644 --- a/lib/pages/laboratory/laboratory_logic.dart +++ b/lib/pages/laboratory/laboratory_logic.dart @@ -9,20 +9,48 @@ import 'package:moodiary/utils/notice_util.dart'; import 'package:share_plus/share_plus.dart'; class LaboratoryLogic extends GetxController { - Future setTencentID({required String id, required String key}) async { - await PrefUtil.setValue('tencentId', id); - await PrefUtil.setValue('tencentKey', key); - update(); + Future setTencentID({required String id}) async { + try { + await PrefUtil.setValue('tencentId', id); + return true; + } catch (e) { + return false; + } finally { + update(); + } } - Future setQweatherKey({required String key}) async { - await PrefUtil.setValue('qweatherKey', key); - update(); + Future setTencentKey({required String key}) async { + try { + await PrefUtil.setValue('tencentKey', key); + return true; + } catch (e) { + return false; + } finally { + update(); + } } - Future setTiandituKey({required String key}) async { - await PrefUtil.setValue('tiandituKey', key); - update(); + Future setQweatherKey({required String key}) async { + try { + await PrefUtil.setValue('qweatherKey', key); + return true; + } catch (e) { + return false; + } finally { + update(); + } + } + + Future setTiandituKey({required String key}) async { + try { + await PrefUtil.setValue('tiandituKey', key); + return true; + } catch (e) { + return false; + } finally { + update(); + } } Future exportErrorLog() async { diff --git a/lib/pages/laboratory/laboratory_view.dart b/lib/pages/laboratory/laboratory_view.dart index 4a8e777..4441ca0 100644 --- a/lib/pages/laboratory/laboratory_view.dart +++ b/lib/pages/laboratory/laboratory_view.dart @@ -1,7 +1,7 @@ -import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter/material.dart'; -import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:gap/gap.dart'; import 'package:get/get.dart'; +import 'package:moodiary/components/base/tile/qr_tile.dart'; import 'package:moodiary/l10n/l10n.dart'; import 'package:moodiary/persistence/pref.dart'; import 'package:moodiary/utils/notice_util.dart'; @@ -20,102 +20,71 @@ class LaboratoryPage extends StatelessWidget { body: GetBuilder( builder: (_) { return ListView( + padding: const EdgeInsets.symmetric(horizontal: 12.0), children: [ - ListTile( - title: const Text('腾讯云密钥'), - isThreeLine: true, - subtitle: SelectionArea( - child: Text( - 'ID:${PrefUtil.getValue('tencentId') ?? ''}\nKey:${PrefUtil.getValue('tencentKey') ?? ''}', - ), - ), - trailing: IconButton( - onPressed: () async { - final res = await showTextInputDialog( - context: context, - textFields: [ - DialogTextField( - hintText: 'ID', - initialText: - PrefUtil.getValue('tencentId') ?? '', - ), - DialogTextField( - hintText: 'KEY', - initialText: - PrefUtil.getValue('tencentKey') ?? '', - ), - ], - title: '腾讯云密钥', - message: '在腾讯云控制台获取密钥', - style: AdaptiveStyle.material, - ); - if (res != null) { - logic.setTencentID(id: res[0], key: res[1]); - } - }, - icon: const FaIcon(FontAwesomeIcons.wrench), - ), + QrInputTile( + title: '${context.l10n.labTencentCloud} ID', + value: PrefUtil.getValue('tencentId') ?? '', + prefix: 'tencentId', + onValue: (value) async { + final res = await logic.setTencentID(id: value); + if (res) { + toast.success(); + } else { + toast.error(); + } + }, ), - ListTile( - title: const Text('和风天气密钥'), - subtitle: SelectionArea( - child: Text(PrefUtil.getValue('qweatherKey') ?? ''), - ), - trailing: IconButton( - onPressed: () async { - final res = await showTextInputDialog( - context: context, - style: AdaptiveStyle.material, - title: '和风天气密钥', - message: '在和风天气控制台获取密钥', - textFields: [ - DialogTextField( - hintText: 'KEY', - initialText: - PrefUtil.getValue('qweatherKey') ?? '', - ), - ], - ); - if (res != null) { - logic.setQweatherKey(key: res[0]); - } - }, - icon: const FaIcon(FontAwesomeIcons.wrench), - ), + const Gap(12), + QrInputTile( + title: '${context.l10n.labTencentCloud} Key', + value: PrefUtil.getValue('tencentKey') ?? '', + prefix: 'tencentKey', + onValue: (value) async { + final res = await logic.setTencentKey(key: value); + if (res) { + toast.success(); + } else { + toast.error(); + } + }, ), - ListTile( - title: const Text('天地图密钥'), - subtitle: SelectionArea( - child: Text(PrefUtil.getValue('tiandituKey') ?? ''), - ), - trailing: IconButton( - onPressed: () async { - final res = await showTextInputDialog( - context: context, - textFields: [ - DialogTextField( - hintText: 'KEY', - initialText: - PrefUtil.getValue('tiandituKey') ?? '', - ), - ], - title: '天地图密钥', - message: '在天地图控制台获取密钥', - style: AdaptiveStyle.material, - ); - if (res != null) { - logic.setTiandituKey(key: res[0]); - } - }, - icon: const FaIcon(FontAwesomeIcons.wrench), - ), + const Gap(12), + QrInputTile( + title: '${context.l10n.labQweather} Key', + value: PrefUtil.getValue('qweatherKey') ?? '', + prefix: 'qweatherKey', + onValue: (value) async { + final res = await logic.setQweatherKey(key: value); + if (res) { + toast.success(); + } else { + toast.error(); + } + }, ), + const Gap(12), + QrInputTile( + title: '${context.l10n.labTianditu} Key', + value: PrefUtil.getValue('tiandituKey') ?? '', + prefix: 'tiandituKey', + onValue: (value) async { + final res = await logic.setTiandituKey(key: value); + if (res) { + toast.success(); + } else { + toast.error(); + } + }, + ), + const Gap(12), ListTile( - onTap: () { + onTap: () async { logic.exportErrorLog(); }, title: const Text('导出日志文件'), ), + const Gap(12), ListTile( onTap: () async { final res = await logic.aesTest(); diff --git a/lib/pages/recycle/recycle_view.dart b/lib/pages/recycle/recycle_view.dart index 30845a4..6ad6f9d 100644 --- a/lib/pages/recycle/recycle_view.dart +++ b/lib/pages/recycle/recycle_view.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:moodiary/components/base/button.dart'; -import 'package:moodiary/components/tile/setting_tile.dart'; +import 'package:moodiary/components/base/tile/setting_tile.dart'; import 'package:moodiary/l10n/l10n.dart'; import 'recycle_logic.dart'; diff --git a/lib/router/app_pages.dart b/lib/router/app_pages.dart index 0b6dcfe..89c268f 100644 --- a/lib/router/app_pages.dart +++ b/lib/router/app_pages.dart @@ -207,6 +207,52 @@ class MoodiaryGetPage extends GetPage { ); } +class MoodiaryFadeInPageRoute extends PageRoute + with MaterialRouteTransitionMixin { + MoodiaryFadeInPageRoute({ + required this.builder, + super.settings, + super.requestFocus, + this.maintainState = true, + super.fullscreenDialog, + super.allowSnapshotting = true, + super.barrierDismissible = false, + }) { + assert(opaque); + } + + final WidgetBuilder builder; + + @override + Widget buildContent(BuildContext context) { + final body = PageAdaptiveBackground( + isHome: settings.name == AppRoutes.homePage, + child: builder(context), + ); + if (Platform.isAndroid || Platform.isIOS) { + return body; + } else { + return Column(children: [const WindowsBar(), Expanded(child: body)]); + } + } + + @override + final bool maintainState; + + @override + Widget buildTransitions( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child, + ) { + return FadeTransition(opacity: animation, child: child); + } + + @override + String get debugLabel => '${super.debugLabel}(${settings.name})'; +} + class _MoodiaryPageTransition implements CustomTransition { final bool? useFade; diff --git a/lib/utils/aes_util.dart b/lib/utils/aes_util.dart index cc412f0..b43ab85 100644 --- a/lib/utils/aes_util.dart +++ b/lib/utils/aes_util.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:typed_data'; +import 'package:dartx/dartx.dart'; import 'package:moodiary/src/rust/api/aes.dart'; class AesUtil { @@ -40,4 +41,55 @@ class AesUtil { ); return utf8.decode(decrypted); } + + /// 基于时间窗口加密 + static Future encryptWithTimeWindow({ + required String data, + required Duration validDuration, + }) async { + final timeSlot = _currentTimeSlot(validDuration); + final dynamicKey = timeSlot.toString().md5; + final salt = _dailySalt(); + + final aesKey = await deriveKey(salt: salt, userKey: dynamicKey); + return await encrypt(key: aesKey, data: data); + } + + /// 基于时间窗口解密 + static Future decryptWithTimeWindow({ + required Uint8List encryptedData, + required Duration validDuration, + int toleranceSlots = 1, + }) async { + final currentSlot = _currentTimeSlot(validDuration); + final salt = _dailySalt(); + + for (int offset = 0; offset <= toleranceSlots; offset++) { + for (final slot in [currentSlot - offset, currentSlot + offset]) { + final dynamicKey = slot.toString().md5; + final aesKey = await deriveKey(salt: salt, userKey: dynamicKey); + try { + final result = await decrypt( + key: aesKey, + encryptedData: encryptedData, + ); + return result; + } catch (_) { + continue; + } + } + } + + return null; + } + + static int _currentTimeSlot(Duration duration) { + final now = DateTime.now().millisecondsSinceEpoch; + return now ~/ duration.inMilliseconds; + } + + static String _dailySalt() { + final date = DateTime.now(); + return '${date.year}-${date.month}-${date.day}'.md5; + } } diff --git a/lib/utils/notice_util.dart b/lib/utils/notice_util.dart index 0723487..665d2fd 100644 --- a/lib/utils/notice_util.dart +++ b/lib/utils/notice_util.dart @@ -20,6 +20,7 @@ class NoticeUtil { usePenetrate: true, displayTime: const Duration(seconds: 2), backType: SmartBackType.ignore, + debounce: true, maskColor: Colors.transparent, builder: (context) { return _build( @@ -70,6 +71,7 @@ class NoticeUtil { maskColor: Colors.transparent, backType: SmartBackType.ignore, usePenetrate: true, + debounce: true, builder: (context) { return _build( context: context, @@ -93,6 +95,7 @@ class NoticeUtil { usePenetrate: true, backType: SmartBackType.ignore, maskColor: Colors.transparent, + debounce: true, builder: (context) { return _build( context: context, diff --git a/lib/utils/theme_util.dart b/lib/utils/theme_util.dart index 8e2b4ca..a8e2122 100644 --- a/lib/utils/theme_util.dart +++ b/lib/utils/theme_util.dart @@ -610,3 +610,25 @@ class ThemeUtil { ); } } + +extension ColorExt on Color { + Brightness get brightness { + final double relativeLuminance = computeLuminance(); + + const double kThreshold = 0.15; + if ((relativeLuminance + 0.05) * (relativeLuminance + 0.05) > kThreshold) { + return Brightness.light; + } + return Brightness.dark; + } +} + +extension ColorExt2 on BuildContext { + Color adaptiveColor(Color color) { + if (!isDarkMode) return color; + + final hsl = HSLColor.fromColor(color); + final inverted = hsl.withLightness(1.0 - hsl.lightness); + return inverted.toColor(); + } +} diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index a912e99..ec24436 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -25,6 +25,7 @@ import local_auth_darwin import macos_ui import macos_window_utils import media_kit_video +import mobile_scanner import network_info_plus import package_info_plus import path_provider_foundation @@ -59,6 +60,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { MacOSUiPlugin.register(with: registry.registrar(forPlugin: "MacOSUiPlugin")) MacOSWindowUtilsPlugin.register(with: registry.registrar(forPlugin: "MacOSWindowUtilsPlugin")) MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin")) + MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) NetworkInfoPlusPlugin.register(with: registry.registrar(forPlugin: "NetworkInfoPlusPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 5174a6b..e9fc743 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -32,6 +32,7 @@ PODS: - Flutter - FlutterMacOS - geolocator_apple (1.2.0): + - Flutter - FlutterMacOS - isar_flutter_libs (1.0.0): - FlutterMacOS @@ -42,12 +43,11 @@ PODS: - FlutterMacOS - macos_window_utils (1.0.0): - FlutterMacOS - - media_kit_libs_macos_video (1.0.4): - - FlutterMacOS - - media_kit_native_event_loop (1.0.0): - - FlutterMacOS - media_kit_video (0.0.1): - FlutterMacOS + - mobile_scanner (7.0.0): + - Flutter + - FlutterMacOS - moodiary_rust (0.0.1): - FlutterMacOS - network_info_plus (0.0.1): @@ -60,11 +60,7 @@ PODS: - FlutterMacOS - quill_native_bridge_macos (0.0.1): - FlutterMacOS - - record_darwin (1.0.0): - - FlutterMacOS - - rive_common (0.0.1): - - FlutterMacOS - - screen_brightness_macos (0.1.0): + - record_macos (1.0.0): - FlutterMacOS - share_plus (0.0.1): - FlutterMacOS @@ -81,6 +77,8 @@ PODS: - video_player_avfoundation (0.0.1): - Flutter - FlutterMacOS + - volume_controller (0.0.1): + - FlutterMacOS - wakelock_plus (0.0.1): - FlutterMacOS @@ -100,28 +98,26 @@ DEPENDENCIES: - flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - gal (from `Flutter/ephemeral/.symlinks/plugins/gal/darwin`) - - geolocator_apple (from `Flutter/ephemeral/.symlinks/plugins/geolocator_apple/macos`) + - geolocator_apple (from `Flutter/ephemeral/.symlinks/plugins/geolocator_apple/darwin`) - isar_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/isar_flutter_libs/macos`) - local_auth_darwin (from `Flutter/ephemeral/.symlinks/plugins/local_auth_darwin/darwin`) - macos_ui (from `Flutter/ephemeral/.symlinks/plugins/macos_ui/macos`) - macos_window_utils (from `Flutter/ephemeral/.symlinks/plugins/macos_window_utils/macos`) - - 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`) + - mobile_scanner (from `Flutter/ephemeral/.symlinks/plugins/mobile_scanner/darwin`) - moodiary_rust (from `Flutter/ephemeral/.symlinks/plugins/moodiary_rust/macos`) - network_info_plus (from `Flutter/ephemeral/.symlinks/plugins/network_info_plus/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`) - quill_native_bridge_macos (from `Flutter/ephemeral/.symlinks/plugins/quill_native_bridge_macos/macos`) - - record_darwin (from `Flutter/ephemeral/.symlinks/plugins/record_darwin/macos`) - - rive_common (from `Flutter/ephemeral/.symlinks/plugins/rive_common/macos`) - - screen_brightness_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_brightness_macos/macos`) + - record_macos (from `Flutter/ephemeral/.symlinks/plugins/record_macos/macos`) - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`) - tflite_flutter (from `Flutter/ephemeral/.symlinks/plugins/tflite_flutter/macos`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - video_player_avfoundation (from `Flutter/ephemeral/.symlinks/plugins/video_player_avfoundation/darwin`) + - volume_controller (from `Flutter/ephemeral/.symlinks/plugins/volume_controller/macos`) - wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`) SPEC REPOS: @@ -160,7 +156,7 @@ EXTERNAL SOURCES: gal: :path: Flutter/ephemeral/.symlinks/plugins/gal/darwin geolocator_apple: - :path: Flutter/ephemeral/.symlinks/plugins/geolocator_apple/macos + :path: Flutter/ephemeral/.symlinks/plugins/geolocator_apple/darwin isar_flutter_libs: :path: Flutter/ephemeral/.symlinks/plugins/isar_flutter_libs/macos local_auth_darwin: @@ -169,12 +165,10 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/macos_ui/macos macos_window_utils: :path: Flutter/ephemeral/.symlinks/plugins/macos_window_utils/macos - media_kit_libs_macos_video: - :path: Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos - media_kit_native_event_loop: - :path: Flutter/ephemeral/.symlinks/plugins/media_kit_native_event_loop/macos media_kit_video: :path: Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos + mobile_scanner: + :path: Flutter/ephemeral/.symlinks/plugins/mobile_scanner/darwin moodiary_rust: :path: Flutter/ephemeral/.symlinks/plugins/moodiary_rust/macos network_info_plus: @@ -185,12 +179,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin quill_native_bridge_macos: :path: Flutter/ephemeral/.symlinks/plugins/quill_native_bridge_macos/macos - record_darwin: - :path: Flutter/ephemeral/.symlinks/plugins/record_darwin/macos - rive_common: - :path: Flutter/ephemeral/.symlinks/plugins/rive_common/macos - screen_brightness_macos: - :path: Flutter/ephemeral/.symlinks/plugins/screen_brightness_macos/macos + record_macos: + :path: Flutter/ephemeral/.symlinks/plugins/record_macos/macos share_plus: :path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos shared_preferences_foundation: @@ -203,6 +193,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos video_player_avfoundation: :path: Flutter/ephemeral/.symlinks/plugins/video_player_avfoundation/darwin + volume_controller: + :path: Flutter/ephemeral/.symlinks/plugins/volume_controller/macos wakelock_plus: :path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos @@ -222,29 +214,27 @@ SPEC CHECKSUMS: flutter_secure_storage_macos: 7f45e30f838cf2659862a4e4e3ee1c347c2b3b54 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 gal: baecd024ebfd13c441269ca7404792a7152fde89 - geolocator_apple: ccfb79d5250de3a295f5093cd03e76aa8836a416 + geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e isar_flutter_libs: a65381780401f81ad6bf3f2e7cd0de5698fb98c4 local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391 macos_ui: 2047a8e6536a80491ef10684c53ca500e04f1bcf macos_window_utils: 3bca8603c2a1cf2257351dfe6bbccc9accf739fd - media_kit_libs_macos_video: 85a23e549b5f480e72cae3e5634b5514bc692f65 - media_kit_native_event_loop: a80d071c835c612fd80173e79390a50ec409f1b1 - media_kit_video: fa6564e3799a0a28bff39442334817088b7ca758 + media_kit_video: 28d7d27611c7769a2464eb7621c37386a61dcc9a + mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93 moodiary_rust: 2f0ea7e60816f68d22e387a10b460860168eced5 network_info_plus: 21d1cd6a015ccb2fdff06a1fbfa88d54b4e92f61 OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 package_info_plus: f0052d280d17aa382b932f399edf32507174e870 path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 quill_native_bridge_macos: 2b005cb56902bb740e0cd9620aa399dfac6b4882 - record_darwin: 30509266ae213af8afdb09a8ae7467cb64c1377e - rive_common: ea79040f86acf053a2d5a75a2506175ee39796a5 - screen_brightness_macos: 2a3ee243f8051c340381e8e51bcedced8360f421 + record_macos: 295d70bd5fb47145df78df7b80e6697cd18403c0 share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 tflite_flutter: d1496f2e968aa5a142fb282da8f5d754fcee5613 url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673 video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b + volume_controller: 5c068e6d085c80dadd33fc2c918d2114b775b3dd wakelock_plus: 21ddc249ac4b8d018838dbdabd65c5976c308497 PODFILE CHECKSUM: b5ff078e9cf81bae88fdc8e0ce3668e57b68e9b6 diff --git a/pubspec.lock b/pubspec.lock index 83e5ddf..27217f5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1226,6 +1226,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.1" + gap: + dependency: "direct main" + description: + name: gap + sha256: f19387d4e32f849394758b91377f9153a1b41d79513ef7668c088c77dbc6955d + url: "https://pub.dev" + source: hosted + version: "3.0.1" geolocator: dependency: "direct main" description: @@ -1779,6 +1787,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.5.7" + mobile_scanner: + dependency: "direct main" + description: + name: mobile_scanner + sha256: "8676156e140c315068cf9b74ceb42b11b239a4895b00914d1d2ca056f70bc917" + url: "https://pub.dev" + source: hosted + version: "7.0.0-beta.9" modal_bottom_sheet: dependency: "direct main" description: @@ -2090,6 +2106,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.0" + qr: + dependency: transitive + description: + name: qr + sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + qr_flutter: + dependency: "direct main" + description: + name: qr_flutter + sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097" + url: "https://pub.dev" + source: hosted + version: "4.1.0" quill_native_bridge: dependency: transitive description: @@ -2575,6 +2607,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + substring_highlight: + dependency: "direct main" + description: + name: substring_highlight + sha256: "96c61e8316098831f6bee87d2386617e4be6aaf87fbc89402dc049d371b67efb" + url: "https://pub.dev" + source: hosted + version: "1.0.33" supabase: dependency: transitive description: @@ -2639,6 +2679,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.11.0" + throttling: + dependency: "direct main" + description: + name: throttling + sha256: e48a4c681b1838b8bf99c1a4f822efe43bb69132f9a56091cd5b7d931c862255 + url: "https://pub.dev" + source: hosted + version: "2.0.1" time: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 1abbd71..da5a347 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -122,6 +122,11 @@ dependencies: audioplayers_android_exo: 0.1.2 scrollview_observer: 1.26.0 flutter_smart_dialog: 4.9.8+7 + substring_highlight: 1.0.33 + qr_flutter: 4.1.0 + mobile_scanner: 7.0.0-beta.9 + throttling: 2.0.1 + gap: 3.0.1 dev_dependencies: flutter_test: