feat(save_image): add save image to local

Closes #46
This commit is contained in:
ZhuJHua
2024-12-14 23:55:40 +08:00
parent 1b9b16b3e1
commit 523535ba16
10 changed files with 121 additions and 136 deletions

View File

@@ -1,63 +1,65 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>心绪日记</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>心绪日记</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>NSFaceIDUsageDescription</key>
<string>Used to apply locks</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Used to apply locks</string>
<key>NSCameraUsageDescription</key>
<string>Used to apply locks</string>
<key>NSMicrophoneUsageDescription</key>
<string>Used to apply locks</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app needs access to location when open.</string>
<key>UIStatusBarHidden</key>
<true/>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>心绪日记</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>心绪日记</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSCameraUsageDescription</key>
<string>Used to apply locks</string>
<key>NSFaceIDUsageDescription</key>
<string>Used to apply locks</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app needs access to location when open.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Used to apply locks</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string></string>
<key>NSPhotoLibraryUsageDescription</key>
<string></string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIStatusBarHidden</key>
<true/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist>

View File

@@ -7,8 +7,6 @@ import 'package:mood_diary/api/api.dart';
import 'package:mood_diary/utils/cache_util.dart';
class WindowButtons extends StatelessWidget {
final ColorScheme colorScheme;
final RxString hitokoto = ''.obs;
//获取一言
@@ -21,10 +19,11 @@ class WindowButtons extends StatelessWidget {
}
}
WindowButtons({super.key, required this.colorScheme});
WindowButtons({super.key});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
//getHitokoto();
final buttonColors = WindowButtonColors(
iconNormal: colorScheme.secondary,

View File

@@ -25,16 +25,18 @@ import 'package:video_player_media_kit/video_player_media_kit.dart';
import 'components/window_buttons/window_buttons.dart';
late final AppLocalizations l10n;
Future<void> initSystem() async {
WidgetsFlutterBinding.ensureInitialized();
await findSystemLocale();
await RustLib.init();
await PrefUtil.initPref();
await IsarUtil.initIsar();
await WebDavUtil().initWebDav();
VideoPlayerMediaKit.ensureInitialized(android: true, iOS: true, macOS: true, windows: true);
await FMTCObjectBoxBackend().initialise();
await const FMTCStore('mapStore').manage.create();
await RustLib.init();
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
systemNavigationBarColor: Colors.transparent,
@@ -81,13 +83,13 @@ void main() async {
onGenerateTitle: (context) => AppLocalizations.of(context)!.appName,
backButtonDispatcher: GetRootBackButtonDispatcher(),
builder: (context, child) {
final colorScheme = Theme.of(context).colorScheme;
l10n = AppLocalizations.of(context)!;
final mediaQuery = MediaQuery(
data: MediaQuery.of(context).copyWith(textScaler: TextScaler.linear(PrefUtil.getValue<double>('fontScale')!)),
child: FToastBuilder()(context, child!),
);
final windowChild = (Platform.isWindows || Platform.isMacOS || Platform.isLinux)
? Column(children: [WindowButtons(colorScheme: colorScheme), Expanded(child: mediaQuery)])
? Column(children: [WindowButtons(), Expanded(child: mediaQuery)])
: mediaQuery;
return windowChild;
},

View File

@@ -5,9 +5,9 @@ import 'package:get/get.dart';
import 'package:mood_diary/common/values/border.dart';
import 'package:mood_diary/common/values/colors.dart';
import 'package:mood_diary/components/diary_card/calendar_diary_card/calendar_diary_card_view.dart';
import 'package:mood_diary/components/loading/loading.dart';
import 'package:mood_diary/components/time_line/time_line_view.dart';
import 'package:mood_diary/utils/array_util.dart';
import 'package:rive_animated_icon/rive_animated_icon.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'calendar_logic.dart';
@@ -230,17 +230,7 @@ class CalendarPage extends StatelessWidget {
Expanded(child: Obx(() {
return AnimatedSwitcher(
duration: const Duration(milliseconds: 400),
child: state.isFetching.value
? Center(
child: RiveAnimatedIcon(
riveIcon: RiveIcon.search,
height: 80,
width: 80,
loopAnimation: true,
strokeWidth: 4,
color: colorScheme.onSurface,
))
: buildCardList(),
child: state.isFetching.value ? const Center(child: Processing()) : buildCardList(),
);
})),
],

View File

@@ -1,10 +1,8 @@
import 'package:flutter/animation.dart';
import 'package:get/get.dart';
import 'package:mood_diary/common/values/media_type.dart';
import 'package:mood_diary/components/audio_player/audio_player_logic.dart';
import 'package:mood_diary/router/app_routes.dart';
import '../../../utils/data/isar.dart';
import '../../../utils/file_util.dart';
import '../../../utils/notice_util.dart';
import 'media_state.dart';
@@ -85,50 +83,7 @@ class MediaLogic extends GetxController with GetSingleTickerProviderStateMixin {
state.isCleaning = true;
update(['modal']);
// 获取各类型的所有文件路径并转换为Set以提高查找效率
final imageFiles = (await FileUtil.getDirFileName(MediaType.image.value)).toSet();
final audioFiles = (await FileUtil.getDirFileName(MediaType.audio.value)).toSet();
final videoFiles = (await FileUtil.getDirFileName(MediaType.video.value)).toSet();
// 用于存储日记中引用的文件名的Set
final usedImages = <String>{};
final usedAudios = <String>{};
final usedVideos = <String>{};
// 获取日记总数
final count = IsarUtil.countAllDiary();
// 分批获取日记并收集引用的文件名
const batchSize = 50;
for (int i = 0; i < count; i += batchSize) {
final diaryList = await IsarUtil.getDiary(i, batchSize);
for (var diary in diaryList) {
usedImages.addAll(diary.imageName);
usedAudios.addAll(diary.audioName);
usedVideos.addAll(diary.videoName);
for (var name in diary.videoName) {
var thumbnailName = 'thumbnail-${name.substring(6, 42)}.jpeg';
usedVideos.add(thumbnailName);
}
}
}
// 计算需要删除的文件
final imagesToDelete = imageFiles.difference(usedImages);
final audiosToDelete = audioFiles.difference(usedAudios);
final videosToDelete = videoFiles.difference(usedVideos);
// delete controller when need
for (var path in audiosToDelete) {
Bind.delete<AudioPlayerLogic>(tag: path);
}
// 并行删除文件
await Future.wait([
FileUtil.deleteMediaFiles(imagesToDelete, MediaType.image.value),
FileUtil.deleteMediaFiles(audiosToDelete, MediaType.audio.value),
FileUtil.deleteMediaFiles(videosToDelete, MediaType.video.value),
]);
await FileUtil.cleanFile();
await getFilePath(state.mediaType.value);
state.isCleaning = false;
update(['modal']);

View File

@@ -2,6 +2,8 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:mood_diary/common/values/media_type.dart';
import 'package:mood_diary/utils/media_util.dart';
import 'package:photo_view/photo_view.dart';
import 'package:photo_view/photo_view_gallery.dart';
@@ -26,6 +28,13 @@ class ImagePage extends StatelessWidget {
style: const TextStyle(color: Colors.white),
),
iconTheme: const IconThemeData(color: Colors.white),
actions: [
IconButton(
onPressed: () {
MediaUtil.saveToGallery(path: state.imagePathList[state.imageIndex], type: MediaType.image);
},
icon: const Icon(Icons.save_alt)),
],
),
body: PhotoViewGallery.builder(
scrollPhysics: const PageScrollPhysics(),

View File

@@ -4,13 +4,16 @@ import 'dart:io';
import 'package:fc_native_video_thumbnail/fc_native_video_thumbnail.dart';
import 'package:flutter/material.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:gal/gal.dart';
import 'package:image_picker/image_picker.dart';
import 'package:mime/mime.dart';
import 'package:mood_diary/src/rust/api/compress.dart';
import 'package:mood_diary/utils/log_util.dart';
import 'package:mood_diary/utils/notice_util.dart';
import 'package:path/path.dart';
import 'package:uuid/uuid.dart';
import '../common/values/media_type.dart';
import '../src/rust/api/constants.dart' as r_type;
import 'data/pref.dart';
import 'file_util.dart';
@@ -113,9 +116,10 @@ class MediaUtil {
// 遍历视频文件
final videoFiles = videoDir.listSync().whereType<File>();
for (final videoFile in videoFiles) {
//如果是缩略图则跳过
if (videoFile.path.contains('thumbnail')) continue;
final videoName = basename(videoFile.path);
final thumbnailPath = getThumbnailPath(videoName);
// 检查是否存在缩略图
if (!File(thumbnailPath).existsSync()) {
LogUtil.printInfo("Thumbnail missing for $videoName. Regenerating...");
@@ -268,4 +272,20 @@ class MediaUtil {
return await _thumbnail.getVideoThumbnail(
srcFile: xFile.path, destFile: destPath, width: height, height: height, format: 'jpeg', quality: 90);
}
// 保存视频或者图片到相册
static Future<void> saveToGallery({required String path, required MediaType type}) async {
final hasAccess = await Gal.hasAccess(toAlbum: true);
if (!hasAccess) await Gal.requestAccess(toAlbum: true);
try {
if (type == MediaType.video) {
await Gal.putVideo(path, album: 'Moodiary');
} else {
await Gal.putImage(path, album: 'Moodiary');
}
NoticeUtil.showToast('已保存到相册');
} catch (e) {
NoticeUtil.showToast('保存失败');
}
}
}

View File

@@ -24,13 +24,17 @@
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>NSHumanReadableCopyright</key>
<string>$(PRODUCT_COPYRIGHT)</string>
<key>NSLocationUsageDescription</key>
<string></string>
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSMicrophoneUsageDescription</key>
<string>Some message to describe why you need this permission</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>This app needs access to location.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string></string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>NSMicrophoneUsageDescription</key>
<string>Some message to describe why you need this permission</string>
<key>NSLocationUsageDescription</key>
<string>This app needs access to location.</string>
</dict>
</plist>

View File

@@ -971,7 +971,7 @@ packages:
source: hosted
version: "2.4.0"
gal:
dependency: transitive
dependency: "direct main"
description:
name: gal
sha256: "54c9b72528efce7c66234f3b6dd01cb0304fd8af8196de15571d7bdddb940977"
@@ -1965,11 +1965,12 @@ packages:
rive_animated_icon:
dependency: "direct main"
description:
name: rive_animated_icon
sha256: f789e9f026c3e5a3bcb369db9baef0637ddd15d9a7c4fe4749505202cdc11942
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.2"
path: "."
ref: HEAD
resolved-ref: "7ab479ea6747480038fc24914d6b642984fd5bc9"
url: "https://github.com/ZhuJHua/rive_animated_icons.git"
source: git
version: "2.0.3"
rive_common:
dependency: transitive
description:

View File

@@ -1,7 +1,7 @@
name: mood_diary
description: "A new Flutter project."
publish_to: 'none'
version: 2.6.2+62
version: 2.6.3+63
environment:
sdk: '>=3.4.0 <4.0.0'
@@ -92,7 +92,10 @@ dependencies:
webdav_client: 1.2.2
confetti: 0.8.0
flutter_native_splash: 2.4.3
rive_animated_icon: 2.0.2
gal: 2.3.0
rive_animated_icon:
git:
url: https://github.com/ZhuJHua/rive_animated_icons.git
network_info_plus: 6.1.2
scrollable_positioned_list: 0.3.8
flutter_localizations: