diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 1683f6e..6f01274 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -1,63 +1,65 @@ - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - 心绪日记 - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - 心绪日记 - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleSignature - ???? - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - CADisableMinimumFrameDurationOnPhone - - UIApplicationSupportsIndirectInputEvents - - NSFaceIDUsageDescription - Used to apply locks - NSPhotoLibraryUsageDescription - Used to apply locks - NSCameraUsageDescription - Used to apply locks - NSMicrophoneUsageDescription - Used to apply locks - NSLocationWhenInUseUsageDescription - This app needs access to location when open. - UIStatusBarHidden - - UIViewControllerBasedStatusBarAppearance - - + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + 心绪日记 + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + 心绪日记 + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + NSCameraUsageDescription + Used to apply locks + NSFaceIDUsageDescription + Used to apply locks + NSLocationWhenInUseUsageDescription + This app needs access to location when open. + NSMicrophoneUsageDescription + Used to apply locks + NSPhotoLibraryAddUsageDescription + + NSPhotoLibraryUsageDescription + + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIStatusBarHidden + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + diff --git a/lib/components/window_buttons/window_buttons.dart b/lib/components/window_buttons/window_buttons.dart index 8696e94..425b928 100644 --- a/lib/components/window_buttons/window_buttons.dart +++ b/lib/components/window_buttons/window_buttons.dart @@ -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, diff --git a/lib/main.dart b/lib/main.dart index e990033..9595f28 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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 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('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; }, diff --git a/lib/pages/home/calendar/calendar_view.dart b/lib/pages/home/calendar/calendar_view.dart index 1f6e6b9..8d115b8 100644 --- a/lib/pages/home/calendar/calendar_view.dart +++ b/lib/pages/home/calendar/calendar_view.dart @@ -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(), ); })), ], diff --git a/lib/pages/home/media/media_logic.dart b/lib/pages/home/media/media_logic.dart index bb2b70d..6ae401b 100644 --- a/lib/pages/home/media/media_logic.dart +++ b/lib/pages/home/media/media_logic.dart @@ -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 = {}; - final usedAudios = {}; - final usedVideos = {}; - - // 获取日记总数 - 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(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']); diff --git a/lib/pages/image/image_view.dart b/lib/pages/image/image_view.dart index 3573bd7..d0050ba 100644 --- a/lib/pages/image/image_view.dart +++ b/lib/pages/image/image_view.dart @@ -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(), diff --git a/lib/utils/media_util.dart b/lib/utils/media_util.dart index 7800679..8f0628b 100644 --- a/lib/utils/media_util.dart +++ b/lib/utils/media_util.dart @@ -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(); 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 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('保存失败'); + } + } } diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist index 606f2e3..fc2db67 100644 --- a/macos/Runner/Info.plist +++ b/macos/Runner/Info.plist @@ -24,13 +24,17 @@ $(MACOSX_DEPLOYMENT_TARGET) NSHumanReadableCopyright $(PRODUCT_COPYRIGHT) + NSLocationUsageDescription + NSMainNibFile MainMenu + NSMicrophoneUsageDescription + Some message to describe why you need this permission + NSPhotoLibraryAddUsageDescription + This app needs access to location. + NSPhotoLibraryUsageDescription + NSPrincipalClass NSApplication - NSMicrophoneUsageDescription - Some message to describe why you need this permission - NSLocationUsageDescription - This app needs access to location. diff --git a/pubspec.lock b/pubspec.lock index dd4beaf..39eb392 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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: diff --git a/pubspec.yaml b/pubspec.yaml index e2b02d4..90f9f7e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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: