mirror of
https://github.com/ZhuJHua/moodiary.git
synced 2026-04-05 16:39:01 +08:00
Merge pull request #233 from ZhuJHua/develop
feat: optimize image processing capabilities
This commit is contained in:
7
.github/workflows/build.yml
vendored
7
.github/workflows/build.yml
vendored
@@ -38,6 +38,13 @@ jobs:
|
||||
with:
|
||||
flutter-version: ${{ env.flutter-version }}
|
||||
|
||||
- name: Set Up Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: nightly
|
||||
override: true
|
||||
|
||||
- name: Install Dependencies
|
||||
run: flutter pub get
|
||||
|
||||
|
||||
28
.github/workflows/cargo-ci.yml
vendored
Normal file
28
.github/workflows/cargo-ci.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
name: Cargo CI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: nightly
|
||||
override: true
|
||||
|
||||
- name: Run cargo test
|
||||
working-directory: rust
|
||||
run: cargo test
|
||||
|
||||
- name: Fail on Errors
|
||||
if: failure()
|
||||
run: exit 1
|
||||
@@ -16,3 +16,7 @@ analyzer:
|
||||
- "**/*.gr.dart"
|
||||
- "**/*.mocks.dart"
|
||||
- "rust_builder/**"
|
||||
- "ios/**"
|
||||
- "android/**"
|
||||
- "windows/**"
|
||||
- "macos/**"
|
||||
|
||||
@@ -1,70 +1,322 @@
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:moodiary/utils/media_util.dart';
|
||||
import 'package:moodiary/utils/cache_util.dart';
|
||||
import 'package:moodiary/utils/log_util.dart';
|
||||
|
||||
class ThumbnailImage extends StatelessWidget {
|
||||
final kTransparentImage = Uint8List.fromList(<int>[
|
||||
0x89,
|
||||
0x50,
|
||||
0x4E,
|
||||
0x47,
|
||||
0x0D,
|
||||
0x0A,
|
||||
0x1A,
|
||||
0x0A,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0D,
|
||||
0x49,
|
||||
0x48,
|
||||
0x44,
|
||||
0x52,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x08,
|
||||
0x06,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x1F,
|
||||
0x15,
|
||||
0xC4,
|
||||
0x89,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0A,
|
||||
0x49,
|
||||
0x44,
|
||||
0x41,
|
||||
0x54,
|
||||
0x78,
|
||||
0x9C,
|
||||
0x63,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x05,
|
||||
0x00,
|
||||
0x01,
|
||||
0x0D,
|
||||
0x0A,
|
||||
0x2D,
|
||||
0xB4,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x49,
|
||||
0x45,
|
||||
0x4E,
|
||||
0x44,
|
||||
0xAE,
|
||||
0x42,
|
||||
0x60,
|
||||
0x82,
|
||||
]);
|
||||
|
||||
enum _ImageLoadState { loading, error, success }
|
||||
|
||||
class _ImageState {
|
||||
final int width;
|
||||
final int height;
|
||||
final String path;
|
||||
final double aspectRatio;
|
||||
|
||||
_ImageState({
|
||||
required this.width,
|
||||
required this.height,
|
||||
required this.path,
|
||||
required this.aspectRatio,
|
||||
});
|
||||
}
|
||||
|
||||
class MoodiaryImage extends StatefulWidget {
|
||||
final String imagePath;
|
||||
final int size;
|
||||
final BoxFit? fit;
|
||||
final VoidCallback? onTap;
|
||||
final String? heroTag;
|
||||
final BorderRadius? borderRadius;
|
||||
final bool showBorder;
|
||||
final EdgeInsets? padding;
|
||||
|
||||
final String heroTag;
|
||||
|
||||
const ThumbnailImage({
|
||||
const MoodiaryImage({
|
||||
super.key,
|
||||
required this.imagePath,
|
||||
required this.size,
|
||||
this.fit,
|
||||
this.onTap,
|
||||
required this.heroTag,
|
||||
this.heroTag,
|
||||
this.borderRadius,
|
||||
this.showBorder = false,
|
||||
this.padding,
|
||||
});
|
||||
|
||||
Widget _buildImage({
|
||||
required double aspectRatio,
|
||||
required ImageProvider imageProvider,
|
||||
required double pixelRatio,
|
||||
}) {
|
||||
final image = Image(
|
||||
key: ValueKey(imagePath),
|
||||
image: ResizeImage(
|
||||
imageProvider,
|
||||
width: aspectRatio < 1.0 ? (size * pixelRatio).toInt() : null,
|
||||
height: aspectRatio >= 1.0 ? (size * pixelRatio).toInt() : null,
|
||||
@override
|
||||
State<MoodiaryImage> createState() => _MoodiaryImageState();
|
||||
}
|
||||
|
||||
class _MoodiaryImageState extends State<MoodiaryImage> {
|
||||
final Rx<_ImageLoadState> _loadState = Rx<_ImageLoadState>(
|
||||
_ImageLoadState.loading,
|
||||
);
|
||||
|
||||
late _ImageState _imageState;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadImage();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_loadState.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant MoodiaryImage oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.imagePath != oldWidget.imagePath ||
|
||||
widget.size != oldWidget.size) {
|
||||
_loadImage();
|
||||
}
|
||||
}
|
||||
|
||||
void _loadImage() async {
|
||||
_loadState.value = _ImageLoadState.loading;
|
||||
try {
|
||||
logger.d('Image loaded from path: ${widget.imagePath}');
|
||||
final imageAspect = await ImageCacheUtil().getImageAspectRatioWithCache(
|
||||
imagePath: widget.imagePath,
|
||||
);
|
||||
|
||||
final imageSize = widget.size;
|
||||
final width =
|
||||
imageAspect < 1.0 ? imageSize : (imageSize * imageAspect).ceil();
|
||||
final height =
|
||||
imageAspect >= 1.0 ? imageSize : (imageSize / imageAspect).ceil();
|
||||
|
||||
final path = await ImageCacheUtil().getLocalImagePathWithCache(
|
||||
imagePath: widget.imagePath,
|
||||
imageWidth: width * 2,
|
||||
imageHeight: height * 2,
|
||||
imageAspectRatio: imageAspect,
|
||||
);
|
||||
|
||||
_imageState = _ImageState(
|
||||
width: width,
|
||||
height: height,
|
||||
path: path,
|
||||
aspectRatio: imageAspect,
|
||||
);
|
||||
|
||||
_loadState.value = _ImageLoadState.success;
|
||||
} catch (e) {
|
||||
_loadState.value = _ImageLoadState.error;
|
||||
}
|
||||
}
|
||||
|
||||
BorderRadius _shrinkBorderRadius(BorderRadius radius, double amount) {
|
||||
return BorderRadius.only(
|
||||
topLeft: Radius.elliptical(
|
||||
(radius.topLeft.x - amount).clamp(0, double.infinity),
|
||||
(radius.topLeft.y - amount).clamp(0, double.infinity),
|
||||
),
|
||||
topRight: Radius.elliptical(
|
||||
(radius.topRight.x - amount).clamp(0, double.infinity),
|
||||
(radius.topRight.y - amount).clamp(0, double.infinity),
|
||||
),
|
||||
bottomLeft: Radius.elliptical(
|
||||
(radius.bottomLeft.x - amount).clamp(0, double.infinity),
|
||||
(radius.bottomLeft.y - amount).clamp(0, double.infinity),
|
||||
),
|
||||
bottomRight: Radius.elliptical(
|
||||
(radius.bottomRight.x - amount).clamp(0, double.infinity),
|
||||
(radius.bottomRight.y - amount).clamp(0, double.infinity),
|
||||
),
|
||||
fit: fit ?? BoxFit.cover,
|
||||
);
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Hero(tag: heroTag, child: image),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final fileImage = FileImage(File(imagePath));
|
||||
final devicePixelRatio = MediaQuery.devicePixelRatioOf(context);
|
||||
const borderWidth = 1.0;
|
||||
|
||||
final Future getAspectRatio = MediaUtil.getImageAspectRatio(fileImage);
|
||||
final loading = ColoredBox(
|
||||
color: context.theme.colorScheme.surfaceContainer,
|
||||
child: const Center(child: Icon(Icons.image_search_rounded)),
|
||||
);
|
||||
return FutureBuilder(
|
||||
future: getAspectRatio,
|
||||
builder: (context, snapshot) {
|
||||
return switch (snapshot.connectionState) {
|
||||
ConnectionState.none => loading,
|
||||
ConnectionState.waiting => loading,
|
||||
ConnectionState.active => loading,
|
||||
ConnectionState.done => _buildImage(
|
||||
aspectRatio: snapshot.data as double,
|
||||
imageProvider: fileImage,
|
||||
pixelRatio: devicePixelRatio,
|
||||
),
|
||||
};
|
||||
},
|
||||
final outerRadius = widget.borderRadius ?? BorderRadius.zero;
|
||||
final innerRadius =
|
||||
widget.showBorder
|
||||
? _shrinkBorderRadius(outerRadius, borderWidth)
|
||||
: outerRadius;
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: outerRadius,
|
||||
border:
|
||||
widget.showBorder
|
||||
? Border.all(
|
||||
color: context.theme.colorScheme.outline.withValues(
|
||||
alpha: 0.6,
|
||||
),
|
||||
width: borderWidth,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
margin: widget.padding,
|
||||
child: ClipRRect(
|
||||
borderRadius: innerRadius,
|
||||
child: AnimatedSwitcher(
|
||||
duration: Durations.short3,
|
||||
switchInCurve: Curves.easeOut,
|
||||
switchOutCurve: Curves.easeIn,
|
||||
child: Obx(() {
|
||||
switch (_loadState.value) {
|
||||
case _ImageLoadState.loading:
|
||||
return const _LoadingPlaceholder(key: ValueKey('loading'));
|
||||
case _ImageLoadState.error:
|
||||
return const _ErrorPlaceholder(key: ValueKey('error'));
|
||||
case _ImageLoadState.success:
|
||||
final imagePath = _imageState.path;
|
||||
final width = _imageState.width;
|
||||
final height = _imageState.height;
|
||||
|
||||
return GestureDetector(
|
||||
key: const ValueKey('image'),
|
||||
onTap:
|
||||
widget.onTap != null
|
||||
? () async {
|
||||
if (widget.heroTag != null) {
|
||||
await precacheImage(
|
||||
FileImage(File(widget.imagePath)),
|
||||
context,
|
||||
);
|
||||
}
|
||||
widget.onTap?.call();
|
||||
}
|
||||
: null,
|
||||
behavior: HitTestBehavior.translucent,
|
||||
child: HeroMode(
|
||||
enabled: widget.heroTag != null,
|
||||
child: Hero(
|
||||
tag: widget.heroTag ?? '',
|
||||
child: FadeInImage(
|
||||
key: ValueKey(imagePath),
|
||||
image: FileImage(File(imagePath)),
|
||||
placeholder: MemoryImage(kTransparentImage),
|
||||
fadeInDuration: Durations.short2,
|
||||
fadeOutDuration: Durations.short1,
|
||||
fit: widget.fit ?? BoxFit.cover,
|
||||
width: width.toDouble(),
|
||||
height: height.toDouble(),
|
||||
imageErrorBuilder: (_, __, ___) {
|
||||
return const _ErrorPlaceholder(
|
||||
key: ValueKey('image_error'),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ErrorPlaceholder extends StatelessWidget {
|
||||
const _ErrorPlaceholder({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ColoredBox(
|
||||
color: context.theme.colorScheme.errorContainer,
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.error_rounded,
|
||||
color: context.theme.colorScheme.onErrorContainer,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LoadingPlaceholder extends StatelessWidget {
|
||||
const _LoadingPlaceholder({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ColoredBox(
|
||||
color: context.theme.colorScheme.surfaceContainer,
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.image_search_rounded,
|
||||
color: context.theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:get/get.dart';
|
||||
@@ -7,6 +5,7 @@ import 'package:intl/intl.dart';
|
||||
import 'package:moodiary/common/models/isar/diary.dart';
|
||||
import 'package:moodiary/common/values/border.dart';
|
||||
import 'package:moodiary/common/values/diary_type.dart';
|
||||
import 'package:moodiary/components/base/image.dart';
|
||||
import 'package:moodiary/components/base/text.dart';
|
||||
import 'package:moodiary/components/diary_card/basic_card_logic.dart';
|
||||
import 'package:moodiary/utils/file_util.dart';
|
||||
@@ -18,33 +17,23 @@ class CalendarDiaryCardComponent extends StatelessWidget with BasicCardLogic {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final pixelRatio = MediaQuery.devicePixelRatioOf(context);
|
||||
|
||||
Widget buildImage() {
|
||||
return SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
spacing: 4.0,
|
||||
spacing: 8.0,
|
||||
children: List.generate(diary.imageName.length, (index) {
|
||||
return SizedBox(
|
||||
height: 100,
|
||||
width: 100,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: ResizeImage(
|
||||
FileImage(
|
||||
File(
|
||||
FileUtil.getRealPath('image', diary.imageName[index]),
|
||||
),
|
||||
),
|
||||
width: (100 * pixelRatio).toInt(),
|
||||
),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
border: Border.all(color: context.theme.colorScheme.outline),
|
||||
borderRadius: AppBorderRadius.smallBorderRadius,
|
||||
height: 100,
|
||||
child: MoodiaryImage(
|
||||
imagePath: FileUtil.getRealPath(
|
||||
'image',
|
||||
diary.imageName[index],
|
||||
),
|
||||
borderRadius: AppBorderRadius.smallBorderRadius,
|
||||
showBorder: true,
|
||||
size: 100,
|
||||
),
|
||||
);
|
||||
}),
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:get/get.dart';
|
||||
@@ -7,6 +5,7 @@ import 'package:intl/intl.dart';
|
||||
import 'package:moodiary/common/models/isar/diary.dart';
|
||||
import 'package:moodiary/common/values/border.dart';
|
||||
import 'package:moodiary/common/values/diary_type.dart';
|
||||
import 'package:moodiary/components/base/image.dart';
|
||||
import 'package:moodiary/components/base/text.dart';
|
||||
import 'package:moodiary/components/diary_card/basic_card_logic.dart';
|
||||
import 'package:moodiary/utils/file_util.dart';
|
||||
@@ -18,22 +17,15 @@ class GirdDiaryCardComponent extends StatelessWidget with BasicCardLogic {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final pixelRatio = MediaQuery.devicePixelRatioOf(context);
|
||||
|
||||
Widget buildImage() {
|
||||
return Container(
|
||||
height: 154.0,
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: ResizeImage(
|
||||
FileImage(
|
||||
File(FileUtil.getRealPath('image', diary.imageName.first)),
|
||||
),
|
||||
width: (250 * pixelRatio).toInt(),
|
||||
),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
return SizedBox(
|
||||
height: 154,
|
||||
child: ClipRRect(
|
||||
borderRadius: AppBorderRadius.mediumBorderRadius,
|
||||
child: MoodiaryImage(
|
||||
imagePath: FileUtil.getRealPath('image', diary.imageName.first),
|
||||
size: 250,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:get/get.dart';
|
||||
@@ -7,6 +5,7 @@ import 'package:intl/intl.dart';
|
||||
import 'package:moodiary/common/models/isar/diary.dart';
|
||||
import 'package:moodiary/common/values/border.dart';
|
||||
import 'package:moodiary/common/values/diary_type.dart';
|
||||
import 'package:moodiary/components/base/image.dart';
|
||||
import 'package:moodiary/components/base/text.dart';
|
||||
import 'package:moodiary/components/diary_card/basic_card_logic.dart';
|
||||
import 'package:moodiary/utils/file_util.dart';
|
||||
@@ -24,22 +23,14 @@ class ListDiaryCardComponent extends StatelessWidget with BasicCardLogic {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final pixelRatio = MediaQuery.devicePixelRatioOf(context);
|
||||
Widget buildImage() {
|
||||
return AspectRatio(
|
||||
aspectRatio: 1.0,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: ResizeImage(
|
||||
FileImage(
|
||||
File(FileUtil.getRealPath('image', diary.imageName.first)),
|
||||
),
|
||||
width: (132 * pixelRatio).toInt(),
|
||||
),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
borderRadius: AppBorderRadius.mediumBorderRadius,
|
||||
child: ClipRRect(
|
||||
borderRadius: AppBorderRadius.mediumBorderRadius,
|
||||
child: MoodiaryImage(
|
||||
imagePath: FileUtil.getRealPath('image', diary.imageName.first),
|
||||
size: 132,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:moodiary/common/values/border.dart';
|
||||
import 'package:moodiary/components/base/image.dart';
|
||||
import 'package:moodiary/pages/image/image_view.dart';
|
||||
import 'package:moodiary/utils/file_util.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
@@ -20,24 +20,23 @@ class MarkdownImageEmbed extends StatelessWidget {
|
||||
final imagePath =
|
||||
isEdit ? imageName : FileUtil.getRealPath('image', imageName);
|
||||
final heroPrefix = const Uuid().v4();
|
||||
final image = MoodiaryImage(
|
||||
imagePath: imagePath,
|
||||
size: 300,
|
||||
heroTag: '${heroPrefix}0',
|
||||
borderRadius: AppBorderRadius.mediumBorderRadius,
|
||||
showBorder: true,
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
onTap: () {
|
||||
if (!isEdit) {
|
||||
showImageView(context, [imagePath], 0, heroTagPrefix: heroPrefix);
|
||||
}
|
||||
},
|
||||
);
|
||||
return Center(
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (!isEdit) {
|
||||
showImageView(context, [imagePath], 0, heroTagPrefix: heroPrefix);
|
||||
}
|
||||
},
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 300),
|
||||
child: Hero(
|
||||
tag: '${heroPrefix}0',
|
||||
child: Card.outlined(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
color: Colors.transparent,
|
||||
child: Image.file(File(imagePath)),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 300),
|
||||
child: image,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ class MediaImageComponent extends StatelessWidget {
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
itemBuilder: (context, index) {
|
||||
final image = ThumbnailImage(
|
||||
final image = MoodiaryImage(
|
||||
imagePath: imageList[index],
|
||||
size: 120,
|
||||
heroTag: '$heroPrefix$index',
|
||||
@@ -66,17 +66,7 @@ class MediaImageComponent extends StatelessWidget {
|
||||
);
|
||||
},
|
||||
);
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
await showImageView(
|
||||
context,
|
||||
imageList,
|
||||
index,
|
||||
heroTagPrefix: heroPrefix,
|
||||
);
|
||||
},
|
||||
child: image,
|
||||
);
|
||||
return image;
|
||||
},
|
||||
itemCount: imageList.length,
|
||||
),
|
||||
|
||||
@@ -61,31 +61,29 @@ class MediaVideoComponent extends StatelessWidget {
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
itemBuilder: (context, index) {
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
await showVideoView(
|
||||
context,
|
||||
videoList,
|
||||
index,
|
||||
heroTagPrefix: '$heroPrefix$index',
|
||||
);
|
||||
},
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: ThumbnailImage(
|
||||
imagePath: thumbnailList[index],
|
||||
heroTag: '$heroPrefix$index',
|
||||
size: 120,
|
||||
),
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: MoodiaryImage(
|
||||
imagePath: thumbnailList[index],
|
||||
heroTag: '$heroPrefix$index',
|
||||
onTap: () async {
|
||||
await showVideoView(
|
||||
context,
|
||||
videoList,
|
||||
index,
|
||||
heroTagPrefix: '$heroPrefix$index',
|
||||
);
|
||||
},
|
||||
size: 120,
|
||||
),
|
||||
const FrostedGlassButton(
|
||||
size: 32,
|
||||
child: Center(child: Icon(Icons.play_arrow_rounded)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const FrostedGlassButton(
|
||||
size: 32,
|
||||
child: Center(child: Icon(Icons.play_arrow_rounded)),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
itemCount: thumbnailList.length,
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_quill/flutter_quill.dart';
|
||||
import 'package:moodiary/common/values/border.dart';
|
||||
import 'package:moodiary/components/base/image.dart';
|
||||
import 'package:moodiary/pages/image/image_view.dart';
|
||||
import 'package:moodiary/utils/file_util.dart';
|
||||
|
||||
@@ -35,31 +34,28 @@ class ImageEmbedBuilder extends EmbedBuilder {
|
||||
isEdit
|
||||
? imageEmbed.name
|
||||
: FileUtil.getRealPath('image', imageEmbed.name);
|
||||
final image = MoodiaryImage(
|
||||
imagePath: imagePath,
|
||||
size: 300,
|
||||
heroTag: '${imageEmbed.name}0',
|
||||
borderRadius: AppBorderRadius.mediumBorderRadius,
|
||||
showBorder: true,
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
onTap: () {
|
||||
if (!isEdit) {
|
||||
showImageView(
|
||||
context,
|
||||
[imagePath],
|
||||
0,
|
||||
heroTagPrefix: imageEmbed.name,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
return Center(
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (!isEdit) {
|
||||
showImageView(
|
||||
context,
|
||||
[imagePath],
|
||||
0,
|
||||
heroTagPrefix: imageEmbed.name,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 300),
|
||||
child: Card.outlined(
|
||||
color: Colors.transparent,
|
||||
child: Hero(
|
||||
tag: '${imageEmbed.name}0',
|
||||
child: ClipRRect(
|
||||
borderRadius: AppBorderRadius.mediumBorderRadius,
|
||||
child: Image.file(File(imagePath)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 300),
|
||||
child: image,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// ignore: unused_import
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
|
||||
import 'app_localizations.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
@@ -18,6 +18,7 @@ import 'package:moodiary/components/window_buttons/window_buttons.dart';
|
||||
import 'package:moodiary/config/env.dart';
|
||||
import 'package:moodiary/l10n/app_localizations.dart';
|
||||
import 'package:moodiary/l10n/l10n.dart';
|
||||
import 'package:moodiary/persistence/hive.dart';
|
||||
import 'package:moodiary/persistence/isar.dart';
|
||||
import 'package:moodiary/persistence/pref.dart';
|
||||
import 'package:moodiary/router/app_pages.dart';
|
||||
@@ -31,11 +32,12 @@ import 'package:video_player_media_kit/video_player_media_kit.dart';
|
||||
|
||||
Future<void> _initSystem() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await RustLib.init();
|
||||
await PrefUtil.initPref();
|
||||
await IsarUtil.initIsar();
|
||||
await ThemeUtil().buildTheme();
|
||||
await WebDavUtil().initWebDav();
|
||||
await HiveUtil().init();
|
||||
unawaited(RustLib.init());
|
||||
unawaited(_platFormOption());
|
||||
WebDavUtil().initWebDav();
|
||||
VideoPlayerMediaKit.ensureInitialized(windows: true);
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||
SystemChrome.setSystemUIOverlayStyle(
|
||||
@@ -44,7 +46,7 @@ Future<void> _initSystem() async {
|
||||
systemNavigationBarContrastEnforced: false,
|
||||
),
|
||||
);
|
||||
await _platFormOption();
|
||||
await ThemeUtil().buildTheme();
|
||||
}
|
||||
|
||||
Future<Locale> _findLanguage() async {
|
||||
|
||||
@@ -15,6 +15,12 @@ class ImageLogic extends GetxController {
|
||||
@override
|
||||
void onInit() {
|
||||
pageController = PageController(initialPage: state.imageIndex.value);
|
||||
pageController.addListener(() {
|
||||
final index = pageController.page?.round() ?? 0;
|
||||
if (state.imageIndex.value != index) {
|
||||
state.imageIndex.value = index;
|
||||
}
|
||||
});
|
||||
super.onInit();
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'dart:io';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:moodiary/persistence/pref.dart';
|
||||
import 'package:moodiary/utils/aes_util.dart';
|
||||
import 'package:moodiary/utils/cache_util.dart';
|
||||
import 'package:moodiary/utils/file_util.dart';
|
||||
import 'package:moodiary/utils/notice_util.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
@@ -75,4 +76,13 @@ class LaboratoryLogic extends GetxController {
|
||||
final decrypted = await AesUtil.decrypt(key: key, encryptedData: encrypted);
|
||||
return decrypted == 'Hello World';
|
||||
}
|
||||
|
||||
Future<bool> clearImageThumbnail() async {
|
||||
try {
|
||||
await ImageCacheUtil().clearImageCache();
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,6 +96,18 @@ class LaboratoryPage extends StatelessWidget {
|
||||
},
|
||||
title: const Text('加密测试'),
|
||||
),
|
||||
const Gap(12),
|
||||
ListTile(
|
||||
onTap: () async {
|
||||
final res = await logic.clearImageThumbnail();
|
||||
if (res) {
|
||||
toast.success(message: '清理成功');
|
||||
} else {
|
||||
toast.error(message: '清理失败');
|
||||
}
|
||||
},
|
||||
title: const Text('清理图片缩略图缓存'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
|
||||
37
lib/persistence/hive.dart
Normal file
37
lib/persistence/hive.dart
Normal file
@@ -0,0 +1,37 @@
|
||||
import 'package:hive_ce/hive.dart';
|
||||
import 'package:hive_ce_flutter/adapters.dart';
|
||||
import 'package:moodiary/utils/file_util.dart';
|
||||
|
||||
class HiveUtil {
|
||||
HiveUtil._();
|
||||
|
||||
static final HiveUtil _instance = HiveUtil._();
|
||||
|
||||
factory HiveUtil() => _instance;
|
||||
|
||||
late LazyBox<bool> _imageCacheBox;
|
||||
|
||||
late LazyBox<double> _imageAspectBox;
|
||||
|
||||
LazyBox<bool> get imageCacheBox => _imageCacheBox;
|
||||
|
||||
LazyBox<double> get imageAspectBox => _imageAspectBox;
|
||||
|
||||
Future<void> init() async {
|
||||
Hive.init(
|
||||
FileUtil.getRealPath('hive', ''),
|
||||
backendPreference: HiveStorageBackendPreference.native,
|
||||
);
|
||||
_imageCacheBox = await Hive.openLazyBox<bool>('image_cache');
|
||||
_imageAspectBox = await Hive.openLazyBox<double>('image_aspect');
|
||||
}
|
||||
|
||||
Future<void> clear() async {
|
||||
await _imageCacheBox.clear();
|
||||
await _imageAspectBox.clear();
|
||||
}
|
||||
|
||||
Future<void> close() async {
|
||||
await Hive.close();
|
||||
}
|
||||
}
|
||||
@@ -213,7 +213,7 @@ class MoodiaryFadeInPageRoute<T> extends PageRoute<T>
|
||||
required this.builder,
|
||||
super.settings,
|
||||
super.requestFocus,
|
||||
this.maintainState = true,
|
||||
this.maintainState = false,
|
||||
super.fullscreenDialog,
|
||||
super.allowSnapshotting = true,
|
||||
super.barrierDismissible = false,
|
||||
|
||||
@@ -9,6 +9,7 @@ import '../frb_generated.dart';
|
||||
import 'constants.dart';
|
||||
|
||||
// These functions are ignored because they are not marked as `pub`: `calculate_target_dimensions`, `load_image`
|
||||
// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `ResizeOptions`
|
||||
|
||||
Future<Uint8List> compress({
|
||||
required DynamicImage img,
|
||||
@@ -29,15 +30,23 @@ abstract class DynamicImage implements RustOpaqueInterface {}
|
||||
|
||||
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<ImageCompress>>
|
||||
abstract class ImageCompress implements RustOpaqueInterface {
|
||||
static Future<Uint8List> contain({
|
||||
static Future<Uint8List> containWithOptions({
|
||||
required String filePath,
|
||||
CompressFormat? compressFormat,
|
||||
int? targetWidth,
|
||||
int? targetHeight,
|
||||
int? minWidth,
|
||||
int? minHeight,
|
||||
int? maxWidth,
|
||||
int? maxHeight,
|
||||
int? quality,
|
||||
}) => RustLib.instance.api.crateApiCompressImageCompressContain(
|
||||
}) => RustLib.instance.api.crateApiCompressImageCompressContainWithOptions(
|
||||
filePath: filePath,
|
||||
compressFormat: compressFormat,
|
||||
targetWidth: targetWidth,
|
||||
targetHeight: targetHeight,
|
||||
minWidth: minWidth,
|
||||
minHeight: minHeight,
|
||||
maxWidth: maxWidth,
|
||||
maxHeight: maxHeight,
|
||||
quality: quality,
|
||||
|
||||
@@ -16,7 +16,7 @@ import 'api/kmp.dart';
|
||||
import 'api/zip.dart';
|
||||
import 'frb_generated.dart';
|
||||
import 'frb_generated.io.dart'
|
||||
if (dart.library.js_interop) 'frb_generated.webated.dart';
|
||||
if (dart.library.js_interop) 'frb_generated.webated.dart';
|
||||
|
||||
/// Main entrypoint of the Rust API
|
||||
class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
|
||||
@@ -69,7 +69,7 @@ class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
|
||||
String get codegenVersion => '2.9.0';
|
||||
|
||||
@override
|
||||
int get rustContentHash => 1427840433;
|
||||
int get rustContentHash => 199828844;
|
||||
|
||||
static const kDefaultExternalLibraryLoaderConfig =
|
||||
ExternalLibraryLoaderConfig(
|
||||
@@ -103,9 +103,13 @@ abstract class RustLibApi extends BaseApi {
|
||||
required String ttfFilePath,
|
||||
});
|
||||
|
||||
Future<Uint8List> crateApiCompressImageCompressContain({
|
||||
Future<Uint8List> crateApiCompressImageCompressContainWithOptions({
|
||||
required String filePath,
|
||||
CompressFormat? compressFormat,
|
||||
int? targetWidth,
|
||||
int? targetHeight,
|
||||
int? minWidth,
|
||||
int? minHeight,
|
||||
int? maxWidth,
|
||||
int? maxHeight,
|
||||
int? quality,
|
||||
@@ -389,9 +393,13 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
);
|
||||
|
||||
@override
|
||||
Future<Uint8List> crateApiCompressImageCompressContain({
|
||||
Future<Uint8List> crateApiCompressImageCompressContainWithOptions({
|
||||
required String filePath,
|
||||
CompressFormat? compressFormat,
|
||||
int? targetWidth,
|
||||
int? targetHeight,
|
||||
int? minWidth,
|
||||
int? minHeight,
|
||||
int? maxWidth,
|
||||
int? maxHeight,
|
||||
int? quality,
|
||||
@@ -405,8 +413,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
compressFormat,
|
||||
serializer,
|
||||
);
|
||||
sse_encode_opt_box_autoadd_i_32(maxWidth, serializer);
|
||||
sse_encode_opt_box_autoadd_i_32(maxHeight, serializer);
|
||||
sse_encode_opt_box_autoadd_u_32(targetWidth, serializer);
|
||||
sse_encode_opt_box_autoadd_u_32(targetHeight, serializer);
|
||||
sse_encode_opt_box_autoadd_u_32(minWidth, serializer);
|
||||
sse_encode_opt_box_autoadd_u_32(minHeight, serializer);
|
||||
sse_encode_opt_box_autoadd_u_32(maxWidth, serializer);
|
||||
sse_encode_opt_box_autoadd_u_32(maxHeight, serializer);
|
||||
sse_encode_opt_box_autoadd_u_8(quality, serializer);
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
@@ -419,19 +431,33 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
decodeSuccessData: sse_decode_list_prim_u_8_strict,
|
||||
decodeErrorData: sse_decode_AnyhowException,
|
||||
),
|
||||
constMeta: kCrateApiCompressImageCompressContainConstMeta,
|
||||
argValues: [filePath, compressFormat, maxWidth, maxHeight, quality],
|
||||
constMeta: kCrateApiCompressImageCompressContainWithOptionsConstMeta,
|
||||
argValues: [
|
||||
filePath,
|
||||
compressFormat,
|
||||
targetWidth,
|
||||
targetHeight,
|
||||
minWidth,
|
||||
minHeight,
|
||||
maxWidth,
|
||||
maxHeight,
|
||||
quality,
|
||||
],
|
||||
apiImpl: this,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
TaskConstMeta get kCrateApiCompressImageCompressContainConstMeta =>
|
||||
TaskConstMeta get kCrateApiCompressImageCompressContainWithOptionsConstMeta =>
|
||||
const TaskConstMeta(
|
||||
debugName: "ImageCompress_contain",
|
||||
debugName: "ImageCompress_contain_with_options",
|
||||
argNames: [
|
||||
"filePath",
|
||||
"compressFormat",
|
||||
"targetWidth",
|
||||
"targetHeight",
|
||||
"minWidth",
|
||||
"minHeight",
|
||||
"maxWidth",
|
||||
"maxHeight",
|
||||
"quality",
|
||||
@@ -994,7 +1020,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
}
|
||||
|
||||
@protected
|
||||
int dco_decode_box_autoadd_i_32(dynamic raw) {
|
||||
int dco_decode_box_autoadd_u_32(dynamic raw) {
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
return raw as int;
|
||||
}
|
||||
@@ -1078,9 +1104,9 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
}
|
||||
|
||||
@protected
|
||||
int? dco_decode_opt_box_autoadd_i_32(dynamic raw) {
|
||||
int? dco_decode_opt_box_autoadd_u_32(dynamic raw) {
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
return raw == null ? null : dco_decode_box_autoadd_i_32(raw);
|
||||
return raw == null ? null : dco_decode_box_autoadd_u_32(raw);
|
||||
}
|
||||
|
||||
@protected
|
||||
@@ -1337,9 +1363,9 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
}
|
||||
|
||||
@protected
|
||||
int sse_decode_box_autoadd_i_32(SseDeserializer deserializer) {
|
||||
int sse_decode_box_autoadd_u_32(SseDeserializer deserializer) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
return (sse_decode_i_32(deserializer));
|
||||
return (sse_decode_u_32(deserializer));
|
||||
}
|
||||
|
||||
@protected
|
||||
@@ -1465,11 +1491,11 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
}
|
||||
|
||||
@protected
|
||||
int? sse_decode_opt_box_autoadd_i_32(SseDeserializer deserializer) {
|
||||
int? sse_decode_opt_box_autoadd_u_32(SseDeserializer deserializer) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
|
||||
if (sse_decode_bool(deserializer)) {
|
||||
return (sse_decode_box_autoadd_i_32(deserializer));
|
||||
return (sse_decode_box_autoadd_u_32(deserializer));
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
@@ -1757,9 +1783,9 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
}
|
||||
|
||||
@protected
|
||||
void sse_encode_box_autoadd_i_32(int self, SseSerializer serializer) {
|
||||
void sse_encode_box_autoadd_u_32(int self, SseSerializer serializer) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
sse_encode_i_32(self, serializer);
|
||||
sse_encode_u_32(self, serializer);
|
||||
}
|
||||
|
||||
@protected
|
||||
@@ -1887,12 +1913,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
}
|
||||
|
||||
@protected
|
||||
void sse_encode_opt_box_autoadd_i_32(int? self, SseSerializer serializer) {
|
||||
void sse_encode_opt_box_autoadd_u_32(int? self, SseSerializer serializer) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
|
||||
sse_encode_bool(self != null, serializer);
|
||||
if (self != null) {
|
||||
sse_encode_box_autoadd_i_32(self, serializer);
|
||||
sse_encode_box_autoadd_u_32(self, serializer);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -143,7 +143,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
CompressFormat dco_decode_box_autoadd_compress_format(dynamic raw);
|
||||
|
||||
@protected
|
||||
int dco_decode_box_autoadd_i_32(dynamic raw);
|
||||
int dco_decode_box_autoadd_u_32(dynamic raw);
|
||||
|
||||
@protected
|
||||
int dco_decode_box_autoadd_u_8(dynamic raw);
|
||||
@@ -185,7 +185,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
CompressFormat? dco_decode_opt_box_autoadd_compress_format(dynamic raw);
|
||||
|
||||
@protected
|
||||
int? dco_decode_opt_box_autoadd_i_32(dynamic raw);
|
||||
int? dco_decode_opt_box_autoadd_u_32(dynamic raw);
|
||||
|
||||
@protected
|
||||
int? dco_decode_opt_box_autoadd_u_8(dynamic raw);
|
||||
@@ -311,7 +311,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
);
|
||||
|
||||
@protected
|
||||
int sse_decode_box_autoadd_i_32(SseDeserializer deserializer);
|
||||
int sse_decode_box_autoadd_u_32(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
int sse_decode_box_autoadd_u_8(SseDeserializer deserializer);
|
||||
@@ -359,7 +359,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
);
|
||||
|
||||
@protected
|
||||
int? sse_decode_opt_box_autoadd_i_32(SseDeserializer deserializer);
|
||||
int? sse_decode_opt_box_autoadd_u_32(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
int? sse_decode_opt_box_autoadd_u_8(SseDeserializer deserializer);
|
||||
@@ -509,7 +509,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_box_autoadd_i_32(int self, SseSerializer serializer);
|
||||
void sse_encode_box_autoadd_u_32(int self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_box_autoadd_u_8(int self, SseSerializer serializer);
|
||||
@@ -569,7 +569,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_opt_box_autoadd_i_32(int? self, SseSerializer serializer);
|
||||
void sse_encode_opt_box_autoadd_u_32(int? self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_opt_box_autoadd_u_8(int? self, SseSerializer serializer);
|
||||
|
||||
@@ -145,7 +145,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
CompressFormat dco_decode_box_autoadd_compress_format(dynamic raw);
|
||||
|
||||
@protected
|
||||
int dco_decode_box_autoadd_i_32(dynamic raw);
|
||||
int dco_decode_box_autoadd_u_32(dynamic raw);
|
||||
|
||||
@protected
|
||||
int dco_decode_box_autoadd_u_8(dynamic raw);
|
||||
@@ -187,7 +187,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
CompressFormat? dco_decode_opt_box_autoadd_compress_format(dynamic raw);
|
||||
|
||||
@protected
|
||||
int? dco_decode_opt_box_autoadd_i_32(dynamic raw);
|
||||
int? dco_decode_opt_box_autoadd_u_32(dynamic raw);
|
||||
|
||||
@protected
|
||||
int? dco_decode_opt_box_autoadd_u_8(dynamic raw);
|
||||
@@ -313,7 +313,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
);
|
||||
|
||||
@protected
|
||||
int sse_decode_box_autoadd_i_32(SseDeserializer deserializer);
|
||||
int sse_decode_box_autoadd_u_32(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
int sse_decode_box_autoadd_u_8(SseDeserializer deserializer);
|
||||
@@ -361,7 +361,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
);
|
||||
|
||||
@protected
|
||||
int? sse_decode_opt_box_autoadd_i_32(SseDeserializer deserializer);
|
||||
int? sse_decode_opt_box_autoadd_u_32(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
int? sse_decode_opt_box_autoadd_u_8(SseDeserializer deserializer);
|
||||
@@ -511,7 +511,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_box_autoadd_i_32(int self, SseSerializer serializer);
|
||||
void sse_encode_box_autoadd_u_32(int self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_box_autoadd_u_8(int self, SseSerializer serializer);
|
||||
@@ -571,7 +571,7 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_opt_box_autoadd_i_32(int? self, SseSerializer serializer);
|
||||
void sse_encode_opt_box_autoadd_u_32(int? self, SseSerializer serializer);
|
||||
|
||||
@protected
|
||||
void sse_encode_opt_box_autoadd_u_8(int? self, SseSerializer serializer);
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:moodiary/persistence/hive.dart';
|
||||
import 'package:moodiary/persistence/pref.dart';
|
||||
import 'package:moodiary/utils/file_util.dart';
|
||||
import 'package:moodiary/utils/log_util.dart';
|
||||
import 'package:moodiary/utils/media_util.dart';
|
||||
import 'package:path/path.dart';
|
||||
|
||||
class CacheUtil {
|
||||
static Future<List<String>?> getCacheList(
|
||||
@@ -39,3 +47,97 @@ class CacheUtil {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ImageCacheUtil {
|
||||
ImageCacheUtil._();
|
||||
|
||||
static final ImageCacheUtil _instance = ImageCacheUtil._();
|
||||
|
||||
factory ImageCacheUtil() => _instance;
|
||||
|
||||
late final _imageCacheBox = HiveUtil().imageCacheBox;
|
||||
|
||||
late final _imageAspectBox = HiveUtil().imageAspectBox;
|
||||
|
||||
Future<void> close() async {
|
||||
await _imageCacheBox.close();
|
||||
}
|
||||
|
||||
Future<void> clearImageCache() async {
|
||||
await _imageCacheBox.clear();
|
||||
await _imageAspectBox.clear();
|
||||
await FileUtil.deleteDir(FileUtil.getRealPath('image_thumbnail', ''));
|
||||
}
|
||||
|
||||
Future<String> getLocalImagePathWithCache({
|
||||
required String imagePath,
|
||||
required int imageWidth,
|
||||
required int imageHeight,
|
||||
required double imageAspectRatio,
|
||||
}) async {
|
||||
final int minSize = min(imageWidth, imageHeight);
|
||||
final int rangeStart = (minSize ~/ 100) * 100;
|
||||
final int rangeEnd = rangeStart + 100;
|
||||
final int baseMinSize = ((rangeStart + rangeEnd) / 2).round();
|
||||
|
||||
final bool isWidthMin = imageWidth < imageHeight;
|
||||
final int standardWidth =
|
||||
isWidthMin ? baseMinSize : (baseMinSize * imageAspectRatio).round();
|
||||
final int standardHeight =
|
||||
isWidthMin ? (baseMinSize / imageAspectRatio).round() : baseMinSize;
|
||||
|
||||
final cachedImageName =
|
||||
'resized_w${standardWidth}_h${standardHeight}_${basename(imagePath)}';
|
||||
final cachedImagePath = FileUtil.getRealPath(
|
||||
'image_thumbnail',
|
||||
cachedImageName,
|
||||
);
|
||||
final cachedImageFile = File(cachedImagePath);
|
||||
|
||||
final bool isCached =
|
||||
(await _imageCacheBox.get(cachedImageName)) == true &&
|
||||
await cachedImageFile.exists();
|
||||
|
||||
if (isCached) {
|
||||
//logger.i('Image cache hit at $cachedImageName');
|
||||
return cachedImagePath;
|
||||
}
|
||||
|
||||
try {
|
||||
final compressedImage = await MediaUtil.compressImageData(
|
||||
imagePath: imagePath,
|
||||
size: baseMinSize,
|
||||
imageAspectRatio: imageAspectRatio,
|
||||
);
|
||||
|
||||
if (compressedImage != null) {
|
||||
final newFile = await cachedImageFile.create(recursive: true);
|
||||
await newFile.writeAsBytes(compressedImage);
|
||||
await _imageCacheBox.put(cachedImageName, true);
|
||||
//logger.i('Image cached at $cachedImageName');
|
||||
return cachedImagePath;
|
||||
}
|
||||
} catch (e) {
|
||||
logger.d('Error compressing image: $e');
|
||||
}
|
||||
|
||||
return imagePath;
|
||||
}
|
||||
|
||||
Future<double> getImageAspectRatioWithCache({
|
||||
required String imagePath,
|
||||
}) async {
|
||||
final cachedAspectRatio = await (_imageAspectBox.get(basename(imagePath)));
|
||||
if (cachedAspectRatio != null) return cachedAspectRatio;
|
||||
try {
|
||||
final aspectRatio = await MediaUtil.getImageAspectRatio(
|
||||
FileImage(File(imagePath)),
|
||||
);
|
||||
await _imageAspectBox.put(basename(imagePath), aspectRatio);
|
||||
return aspectRatio;
|
||||
} catch (e) {
|
||||
logger.d('Error getting image aspect ratio: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,11 +19,11 @@ class LogUtil {
|
||||
);
|
||||
|
||||
void e(message, {required Object error, StackTrace? stackTrace}) {
|
||||
_logger.e(message, error: error, stackTrace: stackTrace);
|
||||
if (kDebugMode) _logger.e(message, error: error, stackTrace: stackTrace);
|
||||
}
|
||||
|
||||
void f(message, {required Object error, StackTrace? stackTrace}) {
|
||||
_logger.f(message, error: error, stackTrace: stackTrace);
|
||||
if (kDebugMode) _logger.f(message, error: error, stackTrace: stackTrace);
|
||||
}
|
||||
|
||||
void i(message) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:fc_native_video_thumbnail/fc_native_video_thumbnail.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -13,6 +14,7 @@ import 'package:moodiary/common/values/media_type.dart';
|
||||
import 'package:moodiary/persistence/pref.dart';
|
||||
import 'package:moodiary/src/rust/api/compress.dart';
|
||||
import 'package:moodiary/src/rust/api/constants.dart' as r_type;
|
||||
import 'package:moodiary/utils/cache_util.dart';
|
||||
import 'package:moodiary/utils/file_util.dart';
|
||||
import 'package:moodiary/utils/log_util.dart';
|
||||
import 'package:moodiary/utils/lru.dart';
|
||||
@@ -20,6 +22,34 @@ import 'package:moodiary/utils/notice_util.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
enum ImageFormat {
|
||||
jpeg(extension: '.jpg'),
|
||||
png(extension: '.png'),
|
||||
heic(extension: '.heic'),
|
||||
webp(extension: '.webp');
|
||||
|
||||
final String extension;
|
||||
|
||||
const ImageFormat({required this.extension});
|
||||
|
||||
static ImageFormat getImageFormat(String imagePath) {
|
||||
final mimeType = lookupMimeType(imagePath);
|
||||
if (mimeType == null) return ImageFormat.png;
|
||||
switch (mimeType) {
|
||||
case 'image/jpeg':
|
||||
return ImageFormat.jpeg;
|
||||
case 'image/png':
|
||||
return ImageFormat.png;
|
||||
case 'image/heic':
|
||||
return ImageFormat.heic;
|
||||
case 'image/webp':
|
||||
return ImageFormat.webp;
|
||||
default:
|
||||
return ImageFormat.png;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MediaUtil {
|
||||
static final _picker = ImagePicker();
|
||||
|
||||
@@ -37,14 +67,6 @@ class MediaUtil {
|
||||
}
|
||||
}
|
||||
|
||||
// 定义 MIME 类型到扩展名和压缩格式的映射
|
||||
static final _compressConfig = {
|
||||
'image/jpeg': ['.jpg', r_type.CompressFormat.jpeg],
|
||||
'image/png': ['.png', r_type.CompressFormat.png],
|
||||
'image/heic': ['.heic', CompressFormat.heic],
|
||||
'image/webp': ['.webp', r_type.CompressFormat.webP],
|
||||
};
|
||||
|
||||
/// 保存图片
|
||||
/// 返回值:
|
||||
/// key:XFile 文件的临时目录
|
||||
@@ -59,15 +81,10 @@ class MediaUtil {
|
||||
imageNameMap[imageFile.path] = basename(imageFile.path);
|
||||
return;
|
||||
}
|
||||
final mimeType =
|
||||
lookupMimeType(imageFile.path) ?? 'image/png'; // 默认使用 PNG
|
||||
final config =
|
||||
_compressConfig[mimeType] ?? ['.png', r_type.CompressFormat.png];
|
||||
final extension = config[0] as String;
|
||||
final format = config[1];
|
||||
final imageName = 'image-${const Uuid().v7()}$extension';
|
||||
final imageFormat = ImageFormat.getImageFormat(imageFile.path);
|
||||
final imageName = 'image-${const Uuid().v7()}${imageFormat.extension}';
|
||||
final outputPath = FileUtil.getRealPath('image', imageName);
|
||||
await _compressImage(imageFile, outputPath, format);
|
||||
await compressAndSaveImage(imageFile, outputPath, imageFormat);
|
||||
imageNameMap[imageFile.path] = imageName;
|
||||
}),
|
||||
);
|
||||
@@ -114,19 +131,8 @@ class MediaUtil {
|
||||
// 保存视频文件
|
||||
await videoFile.saveTo(FileUtil.getRealPath('video', videoName));
|
||||
// 获取缩略图
|
||||
final tempThumbnailPath = FileUtil.getCachePath(
|
||||
'${const Uuid().v7()}.jpeg',
|
||||
);
|
||||
final tempThumbnailPath = FileUtil.getRealPath('thumbnail', videoName);
|
||||
await _getVideoThumbnail(videoFile, tempThumbnailPath);
|
||||
// 压缩缩略图并保存
|
||||
final compressedPath = FileUtil.getRealPath('thumbnail', videoName);
|
||||
await _compressRust(
|
||||
XFile(tempThumbnailPath),
|
||||
compressedPath,
|
||||
r_type.CompressFormat.jpeg,
|
||||
);
|
||||
// 清理临时文件
|
||||
await File(tempThumbnailPath).delete();
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -154,23 +160,8 @@ class MediaUtil {
|
||||
logger.d("Thumbnail missing for $videoName. Regenerating...");
|
||||
|
||||
try {
|
||||
// 生成临时缩略图路径
|
||||
final tempThumbnailPath = FileUtil.getCachePath(
|
||||
'${const Uuid().v7()}.jpeg',
|
||||
);
|
||||
|
||||
// 获取视频缩略图
|
||||
await _getVideoThumbnail(XFile(videoFile.path), tempThumbnailPath);
|
||||
|
||||
// 压缩并保存缩略图
|
||||
await _compressRust(
|
||||
XFile(tempThumbnailPath),
|
||||
thumbnailPath,
|
||||
r_type.CompressFormat.jpeg,
|
||||
);
|
||||
|
||||
// 删除临时文件
|
||||
await File(tempThumbnailPath).delete();
|
||||
await _getVideoThumbnail(XFile(videoFile.path), thumbnailPath);
|
||||
|
||||
logger.d("Thumbnail regenerated for $videoName.");
|
||||
} catch (e) {
|
||||
@@ -209,13 +200,24 @@ class MediaUtil {
|
||||
|
||||
final completer = Completer<double>();
|
||||
final imageStream = imageProvider.resolve(const ImageConfiguration());
|
||||
|
||||
imageStream.addListener(
|
||||
ImageStreamListener((ImageInfo info, bool _) async {
|
||||
final aspectRatio = info.image.width / info.image.height;
|
||||
await _imageAspectRatioCache.put(key, aspectRatio);
|
||||
completer.complete(aspectRatio);
|
||||
}),
|
||||
ImageStreamListener(
|
||||
(ImageInfo info, bool _) async {
|
||||
final aspectRatio = info.image.width / info.image.height;
|
||||
await _imageAspectRatioCache.put(key, aspectRatio);
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete(aspectRatio);
|
||||
}
|
||||
},
|
||||
onError: (Object error, StackTrace? stackTrace) {
|
||||
if (!completer.isCompleted) {
|
||||
completer.completeError(error, stackTrace);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
@@ -256,72 +258,144 @@ class MediaUtil {
|
||||
}
|
||||
|
||||
// 通用压缩逻辑
|
||||
static Future<void> _compressImage(
|
||||
static Future<Uint8List?> compressImageData({
|
||||
required String imagePath,
|
||||
ImageFormat? imageFormat,
|
||||
int? size,
|
||||
double? imageAspectRatio,
|
||||
}) async {
|
||||
final imageFormat_ = imageFormat ?? ImageFormat.getImageFormat(imagePath);
|
||||
return await switch (imageFormat_) {
|
||||
ImageFormat.jpeg => _compressRust(
|
||||
imagePath,
|
||||
r_type.CompressFormat.jpeg,
|
||||
size: size,
|
||||
imageAspectRatio: imageAspectRatio,
|
||||
),
|
||||
ImageFormat.png => _compressRust(
|
||||
imagePath,
|
||||
r_type.CompressFormat.png,
|
||||
size: size,
|
||||
imageAspectRatio: imageAspectRatio,
|
||||
),
|
||||
ImageFormat.heic => _compressNative(
|
||||
imagePath,
|
||||
CompressFormat.heic,
|
||||
size: size,
|
||||
imageAspectRatio: imageAspectRatio,
|
||||
),
|
||||
ImageFormat.webp => _compressRust(
|
||||
imagePath,
|
||||
r_type.CompressFormat.webP,
|
||||
size: size,
|
||||
imageAspectRatio: imageAspectRatio,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/// 压缩图片并保存到指定路径
|
||||
static Future<void> compressAndSaveImage(
|
||||
XFile imageFile,
|
||||
String outputPath,
|
||||
dynamic format,
|
||||
ImageFormat imageFormat,
|
||||
) async {
|
||||
// 如果选择了原图,则直接复制文件
|
||||
if (PrefUtil.getValue<int>('quality') == 3) {
|
||||
await imageFile.saveTo(outputPath);
|
||||
return;
|
||||
}
|
||||
if (format == CompressFormat.heic) {
|
||||
await _compressNative(imageFile, outputPath, format);
|
||||
final newImage = await compressImageData(
|
||||
imagePath: imageFile.path,
|
||||
imageFormat: imageFormat,
|
||||
);
|
||||
if (newImage != null) {
|
||||
await File(outputPath).writeAsBytes(newImage);
|
||||
} else {
|
||||
await _compressRust(imageFile, outputPath, format);
|
||||
await imageFile.saveTo(outputPath);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _compressRust(
|
||||
XFile oldImage,
|
||||
String targetPath,
|
||||
r_type.CompressFormat format,
|
||||
) async {
|
||||
final quality = switch (PrefUtil.getValue<int>('quality')) {
|
||||
0 => 720,
|
||||
1 => 1080,
|
||||
2 => 1440,
|
||||
_ => 1080,
|
||||
};
|
||||
final oldPath = oldImage.path;
|
||||
final newImage = await ImageCompress.contain(
|
||||
filePath: oldPath,
|
||||
maxWidth: quality,
|
||||
maxHeight: quality,
|
||||
compressFormat: format,
|
||||
);
|
||||
await File(targetPath).writeAsBytes(newImage);
|
||||
static Future<Uint8List?> _compressRust(
|
||||
String imagePath,
|
||||
r_type.CompressFormat format, {
|
||||
int? size,
|
||||
double? imageAspectRatio,
|
||||
}) async {
|
||||
final imageSize =
|
||||
size ??
|
||||
switch (PrefUtil.getValue<int>('quality')) {
|
||||
0 => 720,
|
||||
1 => 1080,
|
||||
2 => 1440,
|
||||
_ => 1080,
|
||||
};
|
||||
final oldPath = imagePath;
|
||||
Uint8List? newImage;
|
||||
try {
|
||||
final imageAspect =
|
||||
imageAspectRatio ??
|
||||
await ImageCacheUtil().getImageAspectRatioWithCache(
|
||||
imagePath: imagePath,
|
||||
);
|
||||
|
||||
/// 计算新的宽高
|
||||
/// 对于横图,高度为 size,宽度按比例缩放
|
||||
/// 对于竖图,宽度为 size,高度按比例缩放
|
||||
final width =
|
||||
imageAspect < 1.0 ? imageSize : (imageSize * imageAspect).ceil();
|
||||
final height =
|
||||
imageAspect >= 1.0 ? imageSize : (imageSize / imageAspect).ceil();
|
||||
newImage = await ImageCompress.containWithOptions(
|
||||
filePath: oldPath,
|
||||
targetHeight: height,
|
||||
targetWidth: width,
|
||||
compressFormat: format,
|
||||
);
|
||||
} catch (e) {
|
||||
logger.d('Image compression failed: $e');
|
||||
newImage = null;
|
||||
}
|
||||
return newImage;
|
||||
}
|
||||
|
||||
//图片压缩
|
||||
static Future<void> _compressNative(
|
||||
XFile oldImage,
|
||||
String targetPath,
|
||||
CompressFormat format,
|
||||
) async {
|
||||
static Future<Uint8List?> _compressNative(
|
||||
String imagePath,
|
||||
CompressFormat format, {
|
||||
int? size,
|
||||
double? imageAspectRatio,
|
||||
}) async {
|
||||
if (Platform.isWindows) {
|
||||
oldImage.saveTo(targetPath);
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
final quality = PrefUtil.getValue<int>('quality');
|
||||
final height = switch (quality) {
|
||||
0 => 720,
|
||||
1 => 1080,
|
||||
2 => 1440,
|
||||
_ => 1080,
|
||||
};
|
||||
final imageSize =
|
||||
size ??
|
||||
switch (quality) {
|
||||
0 => 720,
|
||||
1 => 1080,
|
||||
2 => 1440,
|
||||
_ => 1080,
|
||||
};
|
||||
final imageAspect =
|
||||
imageAspectRatio ??
|
||||
await ImageCacheUtil().getImageAspectRatioWithCache(
|
||||
imagePath: imagePath,
|
||||
);
|
||||
|
||||
/// 计算新的宽高
|
||||
/// 对于横图,宽度为 size,高度按比例缩放
|
||||
/// 对于竖图,高度为 size,宽度按比例缩放
|
||||
final width =
|
||||
imageAspect < 1.0 ? imageSize : (imageSize * imageAspect).ceil();
|
||||
final height =
|
||||
imageAspect >= 1.0 ? imageSize : (imageSize / imageAspect).ceil();
|
||||
final newImage = await FlutterImageCompress.compressWithFile(
|
||||
oldImage.path,
|
||||
imagePath,
|
||||
minHeight: height,
|
||||
minWidth: height,
|
||||
minWidth: width,
|
||||
format: format,
|
||||
);
|
||||
if (newImage == null) {
|
||||
oldImage.saveTo(targetPath);
|
||||
return;
|
||||
}
|
||||
await File(targetPath).writeAsBytes(newImage);
|
||||
return newImage;
|
||||
}
|
||||
|
||||
//获取视频缩略图
|
||||
|
||||
@@ -33,7 +33,7 @@ class WebDavUtil {
|
||||
|
||||
factory WebDavUtil() => _instance;
|
||||
|
||||
Future<void> initWebDav() async {
|
||||
void initWebDav() {
|
||||
final webDavOption = options;
|
||||
if (webDavOption.isEmpty) {
|
||||
_client = null;
|
||||
@@ -104,7 +104,7 @@ class WebDavUtil {
|
||||
required String password,
|
||||
}) async {
|
||||
await PrefUtil.setValue('webDavOption', [baseUrl, username, password]);
|
||||
await initWebDav();
|
||||
initWebDav();
|
||||
}
|
||||
|
||||
Future<void> removeWebDavOption() async {
|
||||
|
||||
84
pubspec.lock
84
pubspec.lock
@@ -198,6 +198,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2.0"
|
||||
auto_injector:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: auto_injector
|
||||
sha256: ad7a95d7c381363d48b54e00cb680f024fd97009067244454e9b4850337608e8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
auto_size_text_field:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -754,10 +762,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: fl_chart
|
||||
sha256: "5276944c6ffc975ae796569a826c38a62d2abcf264e26b88fa6f482e107f4237"
|
||||
sha256: f2e9137f261d0f53a820f6b829c80ba570ac915284c8e32789d973834796eca0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.70.2"
|
||||
version: "0.71.0"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
@@ -772,7 +780,7 @@ packages:
|
||||
source: hosted
|
||||
version: "0.3.2"
|
||||
flutter_cache_manager:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_cache_manager
|
||||
sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386"
|
||||
@@ -1048,6 +1056,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.7"
|
||||
flutter_modular:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_modular
|
||||
sha256: bc17a1eb1da676b9111e59d27834fb6673bdea01aead12f0803a0847ff9d451c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.4"
|
||||
flutter_native_splash:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1170,14 +1186,6 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
fluttertoast:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: fluttertoast
|
||||
sha256: "25e51620424d92d3db3832464774a6143b5053f15e382d8ffbfd40b6e795dcf1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.2.12"
|
||||
font_awesome_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1238,18 +1246,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: geolocator
|
||||
sha256: f62bcd90459e63210bbf9c35deb6a51c521f992a78de19a1fe5c11704f9530e2
|
||||
sha256: e7ebfa04ce451daf39b5499108c973189a71a919aa53c1204effda1c5b93b822
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "13.0.4"
|
||||
version: "14.0.0"
|
||||
geolocator_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: geolocator_android
|
||||
sha256: fcb1760a50d7500deca37c9a666785c047139b5f9ee15aa5469fae7dbbe3170d
|
||||
sha256: "114072db5d1dce0ec0b36af2697f55c133bc89a2c8dd513e137c0afe59696ed4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.6.2"
|
||||
version: "5.0.1+1"
|
||||
geolocator_apple:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1355,13 +1363,21 @@ packages:
|
||||
source: hosted
|
||||
version: "0.7.0"
|
||||
hive_ce:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: hive_ce
|
||||
sha256: ac66daee46ad46486a1ed12cf91e9d7479c875fb46889be8d2c96b557406647f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.10.1"
|
||||
hive_ce_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: hive_ce_flutter
|
||||
sha256: "74c1d5f10d803446b4e7913bb272137e2724ba8a56465444f9e7713aeb60a877"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
html:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1803,6 +1819,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
modular_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: modular_core
|
||||
sha256: bd60317c81cff3a510aca19d6ddd661c7c79e3cba97b9f39e9ad199156ff255d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.3.3"
|
||||
moodiary_rust:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -2266,6 +2290,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
result_dart:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: result_dart
|
||||
sha256: "3c69c864a08df0f413a86be211d07405e9a53cc1ac111e3cc8365845a0fb5288"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
retry:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2434,14 +2466,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
sheet:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sheet
|
||||
sha256: f7619b2bd5e031f206d8c22228ef2b12794192dea25b5af0862fa5e37fe6e36d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
shelf:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -2635,18 +2659,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: syncfusion_flutter_core
|
||||
sha256: a9b203c3b4ad5c3bfed3e82c3f43f6861c4f2e8a12dca43b8abb8f128ffe42b5
|
||||
sha256: "98580db6186b46aae965d3f480f7830b0ee6f58c4e1bbbb3fb3de6a121f61836"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "29.1.37"
|
||||
version: "29.1.38"
|
||||
syncfusion_flutter_sliders:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: syncfusion_flutter_sliders
|
||||
sha256: be2d61f6376be8eb3639537f73c2e69eb5837554d1ee369f82a4d1ed9adcaeb7
|
||||
sha256: a0d479786701f505b3c29f48c1c7934e2e0ffe682adb72ed95b39918914eeea7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "29.1.37"
|
||||
version: "29.1.38"
|
||||
synchronized:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -2707,10 +2731,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: tutorial_coach_mark
|
||||
sha256: "2c77c0b00bbe7d5b8a6d31cb9e03d44bf77dfe7ba6514cc2b546886d024a9945"
|
||||
sha256: "9cdb721165d1cfb6e9b1910a1af1b3570fa6caa5059cf1506fcbd00bf7102abf"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.13"
|
||||
version: "1.3.0"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
16
pubspec.yaml
16
pubspec.yaml
@@ -1,7 +1,7 @@
|
||||
name: moodiary
|
||||
description: "A fully open source cross-platform diary app written by flutter and rust."
|
||||
publish_to: 'none'
|
||||
version: 2.7.3+73
|
||||
version: 2.7.3+74
|
||||
|
||||
environment:
|
||||
sdk: '>=3.7.0'
|
||||
@@ -19,7 +19,7 @@ dependencies:
|
||||
logger: 2.5.0
|
||||
flutter_drawing_board: 0.9.8
|
||||
flutter_displaymode: 0.6.0
|
||||
fl_chart: 0.70.2
|
||||
fl_chart: 0.71.0
|
||||
file_picker: 10.1.2
|
||||
local_auth: 2.3.0
|
||||
local_auth_android: 1.0.48
|
||||
@@ -35,14 +35,13 @@ dependencies:
|
||||
crypto: 3.0.6
|
||||
markdown_widget: 2.3.2+6
|
||||
flutter_colorpicker: 1.1.0
|
||||
geolocator: 13.0.4
|
||||
geolocator: 14.0.0
|
||||
shared_preferences: 2.5.3
|
||||
isar: 4.0.0-dev.14
|
||||
isar_flutter_libs:
|
||||
git:
|
||||
url: https://github.com/ZhuJHua/isar
|
||||
path: packages/isar_flutter_libs
|
||||
fluttertoast: 8.2.12
|
||||
cached_network_image: 3.4.1
|
||||
audioplayers: 6.4.0
|
||||
record: 6.0.0
|
||||
@@ -76,7 +75,7 @@ dependencies:
|
||||
encrypt: 5.0.3
|
||||
faker: 2.2.0
|
||||
flutter_rust_bridge: 2.9.0
|
||||
syncfusion_flutter_sliders: 29.1.37
|
||||
syncfusion_flutter_sliders: 29.1.38
|
||||
flutter_quill_extensions: 11.0.0
|
||||
connectivity_plus: 6.1.3
|
||||
image_picker_android: 0.8.12+22
|
||||
@@ -99,8 +98,7 @@ dependencies:
|
||||
moodiary_rust:
|
||||
path: rust_builder
|
||||
modal_bottom_sheet: 3.0.0
|
||||
sheet: 1.0.0
|
||||
tutorial_coach_mark: 1.2.13
|
||||
tutorial_coach_mark: 1.3.0
|
||||
adaptive_dialog: 2.4.1
|
||||
flutter_inappwebview: 6.1.5
|
||||
markdown: 7.3.0
|
||||
@@ -127,6 +125,10 @@ dependencies:
|
||||
mobile_scanner: 7.0.0-beta.9
|
||||
throttling: 2.0.1
|
||||
gap: 3.0.1
|
||||
flutter_cache_manager: 3.4.1
|
||||
hive_ce: 2.10.1
|
||||
hive_ce_flutter: 2.2.0
|
||||
flutter_modular: 6.3.4
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
401
rust/Cargo.lock
generated
401
rust/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -8,12 +8,12 @@ crate-type = ["cdylib", "staticlib"]
|
||||
|
||||
[dependencies]
|
||||
flutter_rust_bridge = "=2.9.0"
|
||||
image = "0.25.5"
|
||||
fast_image_resize = { version = "5.1.2", features = ["image"] }
|
||||
anyhow = "1.0.97"
|
||||
image = "0.25.6"
|
||||
fast_image_resize = { version = "5.1.3", features = ["image"] }
|
||||
anyhow = "1.0.98"
|
||||
ttf-parser = { git = "https://github.com/ZhuJHua/ttf-parser", branch = "fvar" }
|
||||
ring = "0.17.14"
|
||||
zip = "2.4.2"
|
||||
zip = "2.6.1"
|
||||
walkdir = "2.5.0"
|
||||
|
||||
[lints.rust]
|
||||
|
||||
@@ -66,7 +66,6 @@ pub fn compress(
|
||||
}
|
||||
}
|
||||
|
||||
// 返回压缩结果
|
||||
Ok(result_buf.into_inner()?)
|
||||
}
|
||||
|
||||
@@ -74,11 +73,15 @@ pub fn compress(
|
||||
pub struct ImageCompress;
|
||||
|
||||
impl ImageCompress {
|
||||
pub fn contain(
|
||||
pub fn contain_with_options(
|
||||
file_path: String,
|
||||
compress_format: Option<CompressFormat>,
|
||||
max_width: Option<i32>,
|
||||
max_height: Option<i32>,
|
||||
target_width: Option<u32>,
|
||||
target_height: Option<u32>,
|
||||
min_width: Option<u32>,
|
||||
min_height: Option<u32>,
|
||||
max_width: Option<u32>,
|
||||
max_height: Option<u32>,
|
||||
quality: Option<u8>,
|
||||
) -> Result<Vec<u8>> {
|
||||
let src_img = Self::load_image(&file_path)?;
|
||||
@@ -89,41 +92,64 @@ impl ImageCompress {
|
||||
let (dst_width, dst_height) = Self::calculate_target_dimensions(
|
||||
img_width,
|
||||
img_height,
|
||||
max_width.unwrap_or(1024) as u32,
|
||||
max_height.unwrap_or(1024) as u32,
|
||||
&ResizeOptions {
|
||||
target_width,
|
||||
target_height,
|
||||
min_width,
|
||||
min_height,
|
||||
max_width,
|
||||
max_height,
|
||||
},
|
||||
);
|
||||
|
||||
compress(&src_img, dst_height, dst_width, compress_format, quality)
|
||||
}
|
||||
|
||||
fn calculate_target_dimensions(
|
||||
img_width: u32,
|
||||
img_height: u32,
|
||||
options: &ResizeOptions,
|
||||
) -> (u32, u32) {
|
||||
if let (Some(w), Some(h)) = (options.target_width, options.target_height) {
|
||||
return (w, h);
|
||||
}
|
||||
|
||||
let aspect_ratio = img_width as f64 / img_height as f64;
|
||||
|
||||
if let Some(min_w) = options.min_width {
|
||||
let ratio = min_w as f64 / img_width as f64;
|
||||
return (min_w, (img_height as f64 * ratio).round() as u32);
|
||||
}
|
||||
|
||||
if let Some(min_h) = options.min_height {
|
||||
let ratio = min_h as f64 / img_height as f64;
|
||||
return ((img_width as f64 * ratio).round() as u32, min_h);
|
||||
}
|
||||
|
||||
let max_width = options.max_width.unwrap_or(1024);
|
||||
let max_height = options.max_height.unwrap_or(1024);
|
||||
|
||||
if aspect_ratio > 1.0 {
|
||||
let ratio = max_height as f64 / img_height as f64;
|
||||
((img_width as f64 * ratio).round() as u32, max_height)
|
||||
} else {
|
||||
let ratio = max_width as f64 / img_width as f64;
|
||||
(max_width, (img_height as f64 * ratio).round() as u32)
|
||||
}
|
||||
}
|
||||
|
||||
fn load_image(file_path: &str) -> Result<DynamicImage> {
|
||||
ImageReader::open(file_path)?
|
||||
.with_guessed_format()?
|
||||
.decode()
|
||||
.map_err(|e| anyhow::anyhow!("Failed to decode image: {}", e))
|
||||
}
|
||||
|
||||
fn calculate_target_dimensions(
|
||||
img_width: u32,
|
||||
img_height: u32,
|
||||
max_width: u32,
|
||||
max_height: u32,
|
||||
) -> (u32, u32) {
|
||||
// 确保浮点计算
|
||||
let aspect_ratio = img_width as f64 / img_height as f64;
|
||||
|
||||
if aspect_ratio > 1.0 {
|
||||
// 横图,根据 max_height 缩放
|
||||
let ratio = max_height as f64 / img_height as f64;
|
||||
let dst_width = (img_width as f64 * ratio).round() as u32;
|
||||
let dst_height = max_height;
|
||||
(dst_width, dst_height)
|
||||
} else {
|
||||
// 竖图,根据 max_width 缩放
|
||||
let ratio = max_width as f64 / img_width as f64;
|
||||
let dst_width = max_width;
|
||||
let dst_height = (img_height as f64 * ratio).round() as u32;
|
||||
(dst_width, dst_height)
|
||||
}
|
||||
}
|
||||
}
|
||||
pub struct ResizeOptions {
|
||||
pub target_width: Option<u32>,
|
||||
pub target_height: Option<u32>,
|
||||
pub min_width: Option<u32>,
|
||||
pub min_height: Option<u32>,
|
||||
pub max_width: Option<u32>,
|
||||
pub max_height: Option<u32>,
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ flutter_rust_bridge::frb_generated_boilerplate!(
|
||||
default_rust_auto_opaque = RustAutoOpaqueMoi,
|
||||
);
|
||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.9.0";
|
||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 1427840433;
|
||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 199828844;
|
||||
|
||||
// Section: executor
|
||||
|
||||
@@ -230,7 +230,7 @@ fn wire__crate__api__font__FontReader_get_wght_axis_from_vf_font_impl(
|
||||
},
|
||||
)
|
||||
}
|
||||
fn wire__crate__api__compress__ImageCompress_contain_impl(
|
||||
fn wire__crate__api__compress__ImageCompress_contain_with_options_impl(
|
||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||
rust_vec_len_: i32,
|
||||
@@ -238,7 +238,7 @@ fn wire__crate__api__compress__ImageCompress_contain_impl(
|
||||
) {
|
||||
FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::<flutter_rust_bridge::for_generated::SseCodec, _, _>(
|
||||
flutter_rust_bridge::for_generated::TaskInfo {
|
||||
debug_name: "ImageCompress_contain",
|
||||
debug_name: "ImageCompress_contain_with_options",
|
||||
port: Some(port_),
|
||||
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
|
||||
},
|
||||
@@ -255,16 +255,24 @@ fn wire__crate__api__compress__ImageCompress_contain_impl(
|
||||
let api_file_path = <String>::sse_decode(&mut deserializer);
|
||||
let api_compress_format =
|
||||
<Option<crate::api::constants::CompressFormat>>::sse_decode(&mut deserializer);
|
||||
let api_max_width = <Option<i32>>::sse_decode(&mut deserializer);
|
||||
let api_max_height = <Option<i32>>::sse_decode(&mut deserializer);
|
||||
let api_target_width = <Option<u32>>::sse_decode(&mut deserializer);
|
||||
let api_target_height = <Option<u32>>::sse_decode(&mut deserializer);
|
||||
let api_min_width = <Option<u32>>::sse_decode(&mut deserializer);
|
||||
let api_min_height = <Option<u32>>::sse_decode(&mut deserializer);
|
||||
let api_max_width = <Option<u32>>::sse_decode(&mut deserializer);
|
||||
let api_max_height = <Option<u32>>::sse_decode(&mut deserializer);
|
||||
let api_quality = <Option<u8>>::sse_decode(&mut deserializer);
|
||||
deserializer.end();
|
||||
move |context| {
|
||||
transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>(
|
||||
(move || {
|
||||
let output_ok = crate::api::compress::ImageCompress::contain(
|
||||
let output_ok = crate::api::compress::ImageCompress::contain_with_options(
|
||||
api_file_path,
|
||||
api_compress_format,
|
||||
api_target_width,
|
||||
api_target_height,
|
||||
api_min_width,
|
||||
api_min_height,
|
||||
api_max_width,
|
||||
api_max_height,
|
||||
api_quality,
|
||||
@@ -1013,11 +1021,11 @@ impl SseDecode for Option<crate::api::constants::CompressFormat> {
|
||||
}
|
||||
}
|
||||
|
||||
impl SseDecode for Option<i32> {
|
||||
impl SseDecode for Option<u32> {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||
if (<bool>::sse_decode(deserializer)) {
|
||||
return Some(<i32>::sse_decode(deserializer));
|
||||
return Some(<u32>::sse_decode(deserializer));
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
@@ -1112,7 +1120,7 @@ fn pde_ffi_dispatcher_primary_impl(
|
||||
rust_vec_len,
|
||||
data_len,
|
||||
),
|
||||
6 => wire__crate__api__compress__ImageCompress_contain_impl(
|
||||
6 => wire__crate__api__compress__ImageCompress_contain_with_options_impl(
|
||||
port,
|
||||
ptr,
|
||||
rust_vec_len,
|
||||
@@ -1493,12 +1501,12 @@ impl SseEncode for Option<crate::api::constants::CompressFormat> {
|
||||
}
|
||||
}
|
||||
|
||||
impl SseEncode for Option<i32> {
|
||||
impl SseEncode for Option<u32> {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||
<bool>::sse_encode(self.is_some(), serializer);
|
||||
if let Some(value) = self {
|
||||
<i32>::sse_encode(value, serializer);
|
||||
<u32>::sse_encode(value, serializer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod api;
|
||||
mod test;
|
||||
mod frb_generated;
|
||||
|
||||
35
rust/src/test/aes_test.rs
Normal file
35
rust/src/test/aes_test.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
#[cfg(test)]
|
||||
mod aes_test {
|
||||
use crate::api::aes::AesEncryption;
|
||||
|
||||
#[test]
|
||||
fn test_derive_key_consistency() {
|
||||
let salt = "salt123".to_string();
|
||||
let user_key = "password456".to_string();
|
||||
let key1 = AesEncryption::derive_key(salt.clone(), user_key.clone()).unwrap();
|
||||
let key2 = AesEncryption::derive_key(salt, user_key).unwrap();
|
||||
assert_eq!(key1, key2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encrypt_decrypt() {
|
||||
let salt = "testsalt".to_string();
|
||||
let user_key = "testpassword".to_string();
|
||||
let key = AesEncryption::derive_key(salt, user_key).unwrap();
|
||||
|
||||
let original_data = b"Hello Flutter Rust Bridge!".to_vec();
|
||||
let encrypted = AesEncryption::encrypt(key.clone(), original_data.clone()).unwrap();
|
||||
let decrypted = AesEncryption::decrypt(key, encrypted).unwrap();
|
||||
|
||||
assert_eq!(original_data, decrypted);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decrypt_invalid_data() {
|
||||
let key = vec![0u8; 32]; // dummy key
|
||||
let bad_data = vec![1, 2, 3]; // too short
|
||||
|
||||
let result = AesEncryption::decrypt(key, bad_data);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
73
rust/src/test/kmp_test.rs
Normal file
73
rust/src/test/kmp_test.rs
Normal file
@@ -0,0 +1,73 @@
|
||||
#[cfg(test)]
|
||||
mod kmp_test {
|
||||
use crate::api::kmp::{kmp_search, Kmp};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[test]
|
||||
fn test_kmp_basic_match() {
|
||||
let text = "ababcabcababc";
|
||||
let pattern = "abc";
|
||||
let matches = kmp_search(text, pattern);
|
||||
assert_eq!(matches, vec![2, 5, 10]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_replace_with_kmp_single() {
|
||||
let text = "hello world, hello rust";
|
||||
let mut replacements = HashMap::new();
|
||||
replacements.insert("hello".to_string(), "hi".to_string());
|
||||
|
||||
let result = Kmp::replace_with_kmp(text.to_string(), replacements);
|
||||
assert_eq!(result, "hi world, hi rust");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_replace_with_kmp_multiple_overlap() {
|
||||
let text = "abcde";
|
||||
let mut replacements = HashMap::new();
|
||||
replacements.insert("abc".to_string(), "123".to_string());
|
||||
replacements.insert("bcd".to_string(), "234".to_string());
|
||||
|
||||
let result = Kmp::replace_with_kmp(text.to_string(), replacements);
|
||||
// 应优先替换更长的匹配,从左到右,“abc”会匹配成功,然后跳过“bcd”
|
||||
assert_eq!(result, "123de");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_replace_with_kmp_unicode() {
|
||||
let text = "你好世界,世界你好";
|
||||
let mut replacements = HashMap::new();
|
||||
replacements.insert("世界".to_string(), "🌍".to_string());
|
||||
|
||||
let result = Kmp::replace_with_kmp(text.to_string(), replacements);
|
||||
assert_eq!(result, "你好🌍,🌍你好");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_matches() {
|
||||
let text = "flutter and rust are cool";
|
||||
let patterns = vec![
|
||||
"flutter".to_string(),
|
||||
"rust".to_string(),
|
||||
"dart".to_string(),
|
||||
];
|
||||
let result = Kmp::find_matches(text, patterns);
|
||||
assert_eq!(result, vec!["flutter".to_string(), "rust".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_matches_empty() {
|
||||
let text = "no match here";
|
||||
let patterns = vec!["something".to_string(), "nothing".to_string()];
|
||||
let result = Kmp::find_matches(text, patterns);
|
||||
assert!(result.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_replacements() {
|
||||
let text = "keep this";
|
||||
let replacements = HashMap::new();
|
||||
let result = Kmp::replace_with_kmp(text.to_string(), replacements);
|
||||
assert_eq!(result, "keep this");
|
||||
}
|
||||
}
|
||||
2
rust/src/test/mod.rs
Normal file
2
rust/src/test/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
mod aes_test;
|
||||
mod kmp_test;
|
||||
Reference in New Issue
Block a user