feat: add code scanning to import support

This commit is contained in:
ZhuJHua
2025-04-14 15:30:23 +08:00
parent 96682cb593
commit 2eb6ab7409
39 changed files with 1600 additions and 310 deletions

View File

@@ -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"

View File

@@ -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

View File

@@ -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<RenderBox> {
_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;
}
}

View File

@@ -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,
),
],
);
},
);
}

View File

@@ -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<EncryptQrCode> createState() => _EncryptQrCodeState();
}
class _EncryptQrCodeState extends State<EncryptQrCode> {
Uint8List? encryptedData;
late int expireAt; // Unix 时间戳
bool isExpired = false;
@override
void initState() {
super.initState();
_encryptData();
}
Future<void> _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(),
),
);
}
}

View File

@@ -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<String?> showQrScanner({
required BuildContext context,
Duration? validDuration,
String? prefix,
}) async {
return Navigator.push<String?>(
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<QrScanner> createState() => _QrScannerState();
}
class _QrScannerState extends State<QrScanner>
with SingleTickerProviderStateMixin {
late final AnimationController _animationController = AnimationController(
vsync: this,
duration: Durations.long4,
)..repeat(reverse: true);
late final Animation<double> _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<Object?>? _subscription;
late final ValueNotifier<TorchState> _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;
}

View File

@@ -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<String?>(
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,
),
),
],
),
);
}
}

View File

@@ -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)

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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

View File

@@ -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';
}

View File

@@ -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 => '腾讯云';
}

View File

@@ -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"
}

View File

@@ -275,5 +275,18 @@
"mediaVideoCount": "{count} 段视频",
"toastSuccess": "成功",
"toastError": "出错了",
"toastLoading": "加载中"
"toastLoading": "加载中",
"genQrCodeError1": "请先配置 {name}",
"genQrCodeTooltip": "生成二维码",
"qrCodeInvalid": "二维码无效",
"inputTooltip": "输入",
"inputMethodTitle": "输入方式",
"inputMethodScanQrCode": "扫描二维码",
"inputMethodHandelInput": "手动输入",
"getKeyFromConsole": "请从对应控制台获取密钥",
"hasOption": "已配置",
"noOption": "未配置",
"labQweather": "和风天气",
"labTianditu": "天地图",
"labTencentCloud": "腾讯云"
}

View File

@@ -90,4 +90,9 @@ class AboutLogic extends GetxController with GetSingleTickerProviderStateMixin {
void toSponsor() {
Get.toNamed(AppRoutes.sponsorPage);
}
Future<void> toIcp() async {
final uri = Uri.parse('https://beian.miit.gov.cn/');
await launchUrl(uri, mode: LaunchMode.platformDefault);
}
}

View File

@@ -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(

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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<void> checkHasUserKey() async {
state.hasUserKey.value =
(await SecureStorageUtil.getValue('userKey')) != null;
Future<void> checkUserKey() async {
state.userKey.value = (await SecureStorageUtil.getValue('userKey')) ?? '';
}
//获取当前占用储存空间
@@ -135,17 +133,23 @@ class SettingLogic extends GetxController {
Bind.find<DiaryLogic>().updateTitle();
}
Future<void> setUserKey({required String key}) async {
if (key.isNullOrBlank) {
toast.info(message: '密钥不能为空');
return;
Future<bool> 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<void> removeUserKey() async {
await SecureStorageUtil.remove('userKey');
state.hasUserKey.value = false;
Future<bool> removeUserKey() async {
try {
await SecureStorageUtil.remove('userKey');
state.userKey.value = '';
return true;
} catch (e) {
return false;
}
}
}

View File

@@ -22,7 +22,7 @@ class SettingState {
RxBool backendPrivacy = PrefUtil.getValue<bool>('backendPrivacy')!.obs;
RxBool hasUserKey = false.obs;
RxString userKey = ''.obs;
Rx<Language> language =
Language.values

View File

@@ -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<SettingLogic>(
id: 'Lock',
builder: (_) {

View File

@@ -9,20 +9,48 @@ import 'package:moodiary/utils/notice_util.dart';
import 'package:share_plus/share_plus.dart';
class LaboratoryLogic extends GetxController {
Future<void> setTencentID({required String id, required String key}) async {
await PrefUtil.setValue<String>('tencentId', id);
await PrefUtil.setValue<String>('tencentKey', key);
update();
Future<bool> setTencentID({required String id}) async {
try {
await PrefUtil.setValue<String>('tencentId', id);
return true;
} catch (e) {
return false;
} finally {
update();
}
}
Future<void> setQweatherKey({required String key}) async {
await PrefUtil.setValue<String>('qweatherKey', key);
update();
Future<bool> setTencentKey({required String key}) async {
try {
await PrefUtil.setValue<String>('tencentKey', key);
return true;
} catch (e) {
return false;
} finally {
update();
}
}
Future<void> setTiandituKey({required String key}) async {
await PrefUtil.setValue<String>('tiandituKey', key);
update();
Future<bool> setQweatherKey({required String key}) async {
try {
await PrefUtil.setValue<String>('qweatherKey', key);
return true;
} catch (e) {
return false;
} finally {
update();
}
}
Future<bool> setTiandituKey({required String key}) async {
try {
await PrefUtil.setValue<String>('tiandituKey', key);
return true;
} catch (e) {
return false;
} finally {
update();
}
}
Future<void> exportErrorLog() async {

View File

@@ -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<LaboratoryLogic>(
builder: (_) {
return ListView(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
children: [
ListTile(
title: const Text('腾讯云密钥'),
isThreeLine: true,
subtitle: SelectionArea(
child: Text(
'ID:${PrefUtil.getValue<String>('tencentId') ?? ''}\nKey:${PrefUtil.getValue<String>('tencentKey') ?? ''}',
),
),
trailing: IconButton(
onPressed: () async {
final res = await showTextInputDialog(
context: context,
textFields: [
DialogTextField(
hintText: 'ID',
initialText:
PrefUtil.getValue<String>('tencentId') ?? '',
),
DialogTextField(
hintText: 'KEY',
initialText:
PrefUtil.getValue<String>('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<String>('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<String>('qweatherKey') ?? ''),
),
trailing: IconButton(
onPressed: () async {
final res = await showTextInputDialog(
context: context,
style: AdaptiveStyle.material,
title: '和风天气密钥',
message: '在和风天气控制台获取密钥',
textFields: [
DialogTextField(
hintText: 'KEY',
initialText:
PrefUtil.getValue<String>('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<String>('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<String>('tiandituKey') ?? ''),
),
trailing: IconButton(
onPressed: () async {
final res = await showTextInputDialog(
context: context,
textFields: [
DialogTextField(
hintText: 'KEY',
initialText:
PrefUtil.getValue<String>('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<String>('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<String>('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();

View File

@@ -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';

View File

@@ -207,6 +207,52 @@ class MoodiaryGetPage extends GetPage {
);
}
class MoodiaryFadeInPageRoute<T> extends PageRoute<T>
with MaterialRouteTransitionMixin<T> {
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<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
return FadeTransition(opacity: animation, child: child);
}
@override
String get debugLabel => '${super.debugLabel}(${settings.name})';
}
class _MoodiaryPageTransition implements CustomTransition {
final bool? useFade;

View File

@@ -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<Uint8List> 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<String?> 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;
}
}

View File

@@ -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,

View File

@@ -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();
}
}

View File

@@ -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"))

View File

@@ -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

View File

@@ -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:

View File

@@ -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: