├── devtools_options.yaml ├── metadata ├── en-US │ ├── changelogs │ │ ├── 9.txt │ │ ├── 6.txt │ │ ├── 7.txt │ │ ├── 2.txt │ │ ├── 3.txt │ │ ├── 1.txt │ │ ├── 5.txt │ │ ├── 10.txt │ │ ├── 4.txt │ │ └── 8.txt │ ├── short_description.txt │ ├── images │ │ ├── icon.png │ │ └── phoneScreenshots │ │ │ ├── 1.png │ │ │ ├── 2.png │ │ │ └── 3.png │ └── full_description.txt └── zh-CN │ ├── changelogs │ ├── 6.txt │ ├── 7.txt │ ├── 9.txt │ ├── 1.txt │ ├── 2.txt │ ├── 3.txt │ ├── 5.txt │ ├── 10.txt │ ├── 4.txt │ └── 8.txt │ ├── short_description.txt │ ├── full_description.txt │ └── images │ ├── icon.png │ └── phoneScreenshots │ ├── 1.png │ ├── 2.png │ └── 3.png ├── ios ├── Runner │ ├── Runner-Bridging-Header.h │ ├── Assets.xcassets │ │ ├── LaunchImage.imageset │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ ├── README.md │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-50x50@1x.png │ │ │ ├── Icon-App-50x50@2x.png │ │ │ ├── Icon-App-57x57@1x.png │ │ │ ├── Icon-App-57x57@2x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-72x72@1x.png │ │ │ ├── Icon-App-72x72@2x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ └── Contents.json │ ├── Runner.entitlements │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.storyboard │ └── Info.plist ├── Flutter │ ├── Debug.xcconfig │ ├── Release.xcconfig │ └── AppFrameworkInfo.plist ├── Runner.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── WorkspaceSettings.xcsettings │ │ └── IDEWorkspaceChecks.plist ├── RunnerTests │ └── RunnerTests.swift ├── .gitignore ├── Podfile └── Podfile.lock ├── assets ├── icons │ ├── github_mark.png │ ├── snapsaver_icon.png │ └── snapsaver_icon.svg └── sounds │ └── camera_shutter.mp3 ├── l10n.yaml ├── .gitmodules ├── android ├── gradle.properties ├── app │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── launcher_icon.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── launcher_icon.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── launcher_icon.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── launcher_icon.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── launcher_icon.png │ │ │ │ ├── values-zh-rCN │ │ │ │ │ └── strings.xml │ │ │ │ ├── values │ │ │ │ │ ├── strings.xml │ │ │ │ │ └── styles.xml │ │ │ │ ├── drawable │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable-v21 │ │ │ │ │ └── launch_background.xml │ │ │ │ └── values-night │ │ │ │ │ └── styles.xml │ │ │ ├── kotlin │ │ │ │ └── lying │ │ │ │ │ └── fengfeng │ │ │ │ │ └── snapsaver │ │ │ │ │ └── snap_saver │ │ │ │ │ └── MainActivity.kt │ │ │ └── AndroidManifest.xml │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ └── profile │ │ │ └── AndroidManifest.xml │ └── build.gradle ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── .gitignore ├── build.gradle └── settings.gradle ├── lib ├── file │ ├── abs_path_picker.dart │ └── android_native_path_picker.dart ├── dialog │ ├── path_selector_entity.dart │ ├── help_dialog.dart │ ├── remove_saver_dialog.dart │ ├── more_dialog.dart │ └── insert_saver_dialog.dart ├── constants.dart ├── entity │ ├── more.dart │ └── saver.dart ├── viewmodel │ ├── dialog_view_model.dart │ └── home_view_model.dart ├── l10n │ ├── app_zh.arb │ └── app_en.arb ├── db │ └── SaverDatabase.dart ├── main.dart ├── settings_screen.dart └── home_screen.dart ├── .gitignore ├── test └── widget_test.dart ├── README.zh.md ├── .metadata ├── analysis_options.yaml ├── .github └── workflows │ └── main.yml ├── README.md ├── pubspec.yaml ├── LICENSE └── pubspec.lock /devtools_options.yaml: -------------------------------------------------------------------------------- 1 | extensions: 2 | -------------------------------------------------------------------------------- /metadata/en-US/changelogs/9.txt: -------------------------------------------------------------------------------- 1 | bugfix -------------------------------------------------------------------------------- /metadata/zh-CN/changelogs/6.txt: -------------------------------------------------------------------------------- 1 | 实现了手动对焦 -------------------------------------------------------------------------------- /metadata/zh-CN/changelogs/7.txt: -------------------------------------------------------------------------------- 1 | 实现滑动条变焦 -------------------------------------------------------------------------------- /metadata/zh-CN/changelogs/9.txt: -------------------------------------------------------------------------------- 1 | bugfix -------------------------------------------------------------------------------- /metadata/zh-CN/changelogs/1.txt: -------------------------------------------------------------------------------- 1 | 发布第一个版本☝️ -------------------------------------------------------------------------------- /metadata/zh-CN/changelogs/2.txt: -------------------------------------------------------------------------------- 1 | 现在可以切换镜头了 -------------------------------------------------------------------------------- /metadata/zh-CN/changelogs/3.txt: -------------------------------------------------------------------------------- 1 | 添加一些振动效果 -------------------------------------------------------------------------------- /metadata/zh-CN/changelogs/5.txt: -------------------------------------------------------------------------------- 1 | 支持设置拍照键拍出的照片名称 -------------------------------------------------------------------------------- /metadata/en-US/changelogs/6.txt: -------------------------------------------------------------------------------- 1 | Implement manual focus -------------------------------------------------------------------------------- /metadata/en-US/changelogs/7.txt: -------------------------------------------------------------------------------- 1 | Implement slider zoom -------------------------------------------------------------------------------- /metadata/en-US/changelogs/2.txt: -------------------------------------------------------------------------------- 1 | Now you can switch the lens -------------------------------------------------------------------------------- /metadata/en-US/changelogs/3.txt: -------------------------------------------------------------------------------- 1 | Add some vibration effect -------------------------------------------------------------------------------- /metadata/zh-CN/short_description.txt: -------------------------------------------------------------------------------- 1 | 一个可以帮你快速拍照并整理到指定目录中的应用 -------------------------------------------------------------------------------- /metadata/en-US/changelogs/1.txt: -------------------------------------------------------------------------------- 1 | Release the first version ☝️ -------------------------------------------------------------------------------- /metadata/en-US/changelogs/5.txt: -------------------------------------------------------------------------------- 1 | Be able to set photo name for Saver -------------------------------------------------------------------------------- /metadata/zh-CN/changelogs/10.txt: -------------------------------------------------------------------------------- 1 | 1. 固定照片分辨率为4:3 2 | 2. 调整变焦画框和切换前后摄像头的位置 -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /metadata/zh-CN/changelogs/4.txt: -------------------------------------------------------------------------------- 1 | 支持拍照键颜色选择🎨 2 | 支持删除拍照键 3 | 支持在一个拍照键中存储多个路径 -------------------------------------------------------------------------------- /metadata/zh-CN/changelogs/8.txt: -------------------------------------------------------------------------------- 1 | 新增调整预览窗位置功能 2 | 新增调整APP颜色主题功能 3 | 新增调整照片分辨率功能 -------------------------------------------------------------------------------- /metadata/zh-CN/full_description.txt: -------------------------------------------------------------------------------- 1 | 你可以通过这个应用创建拍照键,制定照片的存储目录;后续只需要按下这个拍照键,就可以拍下一张照片并且将它存储到事先设置好的存储目录 -------------------------------------------------------------------------------- /assets/icons/github_mark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NielsLee/SnapSaver/HEAD/assets/icons/github_mark.png -------------------------------------------------------------------------------- /l10n.yaml: -------------------------------------------------------------------------------- 1 | arb-dir: lib/l10n 2 | template-arb-file: app_en.arb 3 | output-localization-file: app_localizations.dart -------------------------------------------------------------------------------- /metadata/en-US/short_description.txt: -------------------------------------------------------------------------------- 1 | An app to quickly take photos and organize them into specified directories. -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule ".flutter"] 2 | path = .flutter 3 | url = https://github.com/flutter/flutter.git 4 | 5 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4G 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /assets/icons/snapsaver_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NielsLee/SnapSaver/HEAD/assets/icons/snapsaver_icon.png -------------------------------------------------------------------------------- /assets/sounds/camera_shutter.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NielsLee/SnapSaver/HEAD/assets/sounds/camera_shutter.mp3 -------------------------------------------------------------------------------- /metadata/en-US/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NielsLee/SnapSaver/HEAD/metadata/en-US/images/icon.png -------------------------------------------------------------------------------- /metadata/zh-CN/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NielsLee/SnapSaver/HEAD/metadata/zh-CN/images/icon.png -------------------------------------------------------------------------------- /lib/file/abs_path_picker.dart: -------------------------------------------------------------------------------- 1 | abstract class PathPicker { 2 | 3 | void selectPath(void Function (String?) callback); 4 | 5 | } -------------------------------------------------------------------------------- /metadata/en-US/changelogs/10.txt: -------------------------------------------------------------------------------- 1 | 1. Fixed photo resolution to 4:3 2 | 2. Adjusted zoom frame and front/back camera switch positions -------------------------------------------------------------------------------- /metadata/en-US/changelogs/4.txt: -------------------------------------------------------------------------------- 1 | Support Saver color selection 🎨 2 | Support Saver deletion 3 | Support storing multiple paths in one Saver -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NielsLee/SnapSaver/HEAD/metadata/en-US/images/phoneScreenshots/1.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NielsLee/SnapSaver/HEAD/metadata/en-US/images/phoneScreenshots/2.png -------------------------------------------------------------------------------- /metadata/en-US/images/phoneScreenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NielsLee/SnapSaver/HEAD/metadata/en-US/images/phoneScreenshots/3.png -------------------------------------------------------------------------------- /metadata/zh-CN/images/phoneScreenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NielsLee/SnapSaver/HEAD/metadata/zh-CN/images/phoneScreenshots/1.png -------------------------------------------------------------------------------- /metadata/zh-CN/images/phoneScreenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NielsLee/SnapSaver/HEAD/metadata/zh-CN/images/phoneScreenshots/2.png -------------------------------------------------------------------------------- /metadata/zh-CN/images/phoneScreenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NielsLee/SnapSaver/HEAD/metadata/zh-CN/images/phoneScreenshots/3.png -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NielsLee/SnapSaver/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NielsLee/SnapSaver/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NielsLee/SnapSaver/HEAD/android/app/src/main/res/mipmap-hdpi/launcher_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NielsLee/SnapSaver/HEAD/android/app/src/main/res/mipmap-mdpi/launcher_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NielsLee/SnapSaver/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NielsLee/SnapSaver/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NielsLee/SnapSaver/HEAD/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NielsLee/SnapSaver/HEAD/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NielsLee/SnapSaver/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NielsLee/SnapSaver/HEAD/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-zh-rCN/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 快存相机 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | SnapSaver 4 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NielsLee/SnapSaver/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NielsLee/SnapSaver/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NielsLee/SnapSaver/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NielsLee/SnapSaver/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NielsLee/SnapSaver/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NielsLee/SnapSaver/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NielsLee/SnapSaver/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NielsLee/SnapSaver/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NielsLee/SnapSaver/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NielsLee/SnapSaver/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NielsLee/SnapSaver/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NielsLee/SnapSaver/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NielsLee/SnapSaver/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NielsLee/SnapSaver/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NielsLee/SnapSaver/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NielsLee/SnapSaver/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NielsLee/SnapSaver/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NielsLee/SnapSaver/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NielsLee/SnapSaver/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NielsLee/SnapSaver/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NielsLee/SnapSaver/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NielsLee/SnapSaver/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /lib/dialog/path_selector_entity.dart: -------------------------------------------------------------------------------- 1 | class PathSelectorEntity { 2 | bool isPathSelected; 3 | String? path; 4 | 5 | PathSelectorEntity({this.isPathSelected = false, this.path}); 6 | } 7 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NielsLee/SnapSaver/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NielsLee/SnapSaver/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /metadata/en-US/changelogs/8.txt: -------------------------------------------------------------------------------- 1 | Added the function of adjusting the preview window position 2 | Added the function of adjusting the APP color theme 3 | Added the function of adjusting the photo resolution -------------------------------------------------------------------------------- /metadata/en-US/full_description.txt: -------------------------------------------------------------------------------- 1 | You can use this app to create a Saver button and set a storage directory for photos; later, just press the Saver button to take a photo and store it in the pre-set storage directory. -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/lying/fengfeng/snapsaver/snap_saver/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package lying.fengfeng.snapsaver.snap_saver 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() 6 | -------------------------------------------------------------------------------- /lib/constants.dart: -------------------------------------------------------------------------------- 1 | class Constants { 2 | static const String dbName = "savers_database.db"; 3 | static const String saverTableName = "savers"; 4 | static const String pathTableName = "saver_paths"; 5 | static const int dbVersion = 3; 6 | } 7 | -------------------------------------------------------------------------------- /ios/Runner/Runner.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.application-groups 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /lib/entity/more.dart: -------------------------------------------------------------------------------- 1 | class More { 2 | final String? photoName; 3 | /** 4 | * 0: index 5 | * 1: timeStamp 6 | * 2: _index 7 | * 3: _timeStamp 8 | * 4: -index 9 | * 5: -timeStamp 10 | */ 11 | final int suffixType; 12 | 13 | const More({this.photoName, this.suffixType = 0}); 14 | } 15 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /ios/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /lib/entity/saver.dart: -------------------------------------------------------------------------------- 1 | class Saver { 2 | final List paths; 3 | final String name; 4 | final int? color; 5 | int count; 6 | final String? photoName; 7 | final int suffixType; 8 | 9 | Saver( 10 | {required this.paths, 11 | required this.name, 12 | this.color, 13 | this.count = 0, 14 | this.photoName, 15 | this.suffixType = 0}); 16 | } 17 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /lib/file/android_native_path_picker.dart: -------------------------------------------------------------------------------- 1 | import 'package:file_picker/file_picker.dart'; 2 | 3 | import 'abs_path_picker.dart'; 4 | 5 | class AndroidNativePathPicker extends PathPicker { 6 | 7 | @override 8 | void selectPath(void Function(String? p1) callback) async { 9 | String? selectedDirectory = await FilePicker.platform.getDirectoryPath(); 10 | callback(selectedDirectory); 11 | } 12 | } -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /lib/dialog/help_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class HelpDialog extends StatelessWidget { 5 | @override 6 | Widget build(BuildContext context) { 7 | return AlertDialog( 8 | title: Text(AppLocalizations.of(context)!.howToUse), 9 | content: Text(AppLocalizations.of(context)!.helpContent), 10 | ); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | maven { url 'https://storage.googleapis.com/download.flutter.io' } 4 | google() 5 | mavenCentral() 6 | } 7 | } 8 | 9 | rootProject.buildDir = '../build' 10 | subprojects { 11 | project.buildDir = "${rootProject.buildDir}/${project.name}" 12 | } 13 | subprojects { 14 | project.evaluationDependsOn(':app') 15 | } 16 | 17 | tasks.register("clean", Delete) { 18 | delete rootProject.buildDir 19 | } 20 | 21 | buildscript { 22 | ext.kotlin_version = '1.7.10' 23 | repositories { 24 | google() 25 | mavenCentral() 26 | } 27 | 28 | dependencies { 29 | classpath 'com.android.tools.build:gradle:7.3.0' 30 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Symbolication related 35 | app.*.symbols 36 | 37 | # Obfuscation related 38 | app.*.map.json 39 | 40 | # Android Studio will place build artifacts here 41 | /android/app/debug 42 | /android/app/profile 43 | /android/app/release 44 | 45 | *.jks 46 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 12.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return flutterSdkPath 8 | } 9 | settings.ext.flutterSdkPath = flutterSdkPath() 10 | 11 | includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") 12 | 13 | repositories { 14 | google() 15 | mavenCentral() 16 | gradlePluginPortal() 17 | } 18 | } 19 | 20 | plugins { 21 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" 22 | id "com.android.application" version "8.7.0" apply false 23 | id "org.jetbrains.kotlin.android" version "1.9.10" apply false 24 | } 25 | 26 | include ":app" 27 | -------------------------------------------------------------------------------- /lib/viewmodel/dialog_view_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | 3 | class DialogViewModel extends ChangeNotifier { 4 | String _name = "Saver Name"; 5 | List _paths = []; 6 | Color? _color = null; 7 | String? _photoName; 8 | int _suffixType = 0; 9 | 10 | String getName() { 11 | return _name; 12 | } 13 | 14 | List getPath() { 15 | return _paths; 16 | } 17 | 18 | Color? getColor() { 19 | return _color; 20 | } 21 | 22 | String? getPhotoName() { 23 | return _photoName; 24 | } 25 | 26 | int getSuffixType() { 27 | return _suffixType; 28 | } 29 | 30 | void setName(name) { 31 | _name = name; 32 | } 33 | 34 | void addPath(path) { 35 | _paths.add(path); 36 | } 37 | 38 | void setColor(color) { 39 | _color = color; 40 | } 41 | 42 | void setPhotoName(name) { 43 | _photoName = name; 44 | } 45 | 46 | void setSuffixType(type) { 47 | _suffixType = type; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility in the flutter_test package. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:snap_saver/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(const MainApp()); 17 | 18 | // Verify that our counter starts at 0. 19 | expect(find.text('0'), findsOneWidget); 20 | expect(find.text('1'), findsNothing); 21 | 22 | // Tap the '+' icon and trigger a frame. 23 | await tester.tap(find.byIcon(Icons.add)); 24 | await tester.pump(); 25 | 26 | // Verify that our counter has incremented. 27 | expect(find.text('0'), findsNothing); 28 | expect(find.text('1'), findsOneWidget); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /README.zh.md: -------------------------------------------------------------------------------- 1 | # 快存相机 2 | 3 | 一款相机应用程序,可以更轻松地将照片拍摄到选定的相册或目录。 4 | 5 |
6 | 2 7 | 2 8 | 3 9 |
10 | 11 | ## 入门指南 12 | 13 | [Get it on F-Droid](https://f-droid.org/packages/lying.fengfeng.snapsaver/) 16 | 或者从[发布记录](https://github.com/NielsLee/SnapSaver/releases/latest)下载最新版本. 17 | 18 | 1. 启动应用并授予相机权限。 19 | 2. 点击右下角的浮动按钮,在弹出的对话框中选择存储路径和拍照键的名称。 20 | 3. 输入信息后,点击确定,拍照键将保留在应用程序的主页上。 21 | 4. 按下刚创建的拍照键,相机预览界面上的照片将被拍摄并保存到 Saver 按钮保存的路径。 22 | 5. 您可以创建任意数量的拍照键,为它们分配不同的路径,然后可以从应用主页轻松拍摄照片并将结果保存到不同的路径! 23 | 24 | ## 注意事项 25 | 26 | **在很多安卓设备上,图片拍摄并存储到指定的目录中后,无法立刻被相册获取到。因此可能需要等待一段时间甚至重启以后才能在相册APP中看到刚刚拍摄的照片。作者凭兴趣开发这款软件,无法投入太多精力,因此暂时没有好的方案解决这个问题。如果正在浏览的各位有好的方案,可以通过Issue与作者交流。十分感谢!😊** 27 | 28 | Buy Me a Coffee at ko-fi.com 29 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: "54e66469a933b60ddf175f858f82eaeb97e48c8d" 8 | channel: "stable" 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d 17 | base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d 18 | - platform: android 19 | create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d 20 | base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d 21 | - platform: ios 22 | create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d 23 | base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d 24 | 25 | # User provided section 26 | 27 | # List of Local paths (relative to this file) that should be 28 | # ignored by the migrate tool. 29 | # 30 | # Files that are not part of the templates will be ignored by default. 31 | unmanaged_files: 32 | - 'lib/main.dart' 33 | - 'ios/Runner.xcodeproj/project.pbxproj' 34 | -------------------------------------------------------------------------------- /lib/l10n/app_zh.arb: -------------------------------------------------------------------------------- 1 | { 2 | "appTitle": "快存相机", 3 | "howToUse": "如何使用?", 4 | "helpContent": "首先,点击右下角的按钮,输入图片保存路径和名称,创建一个新的拍照键;然后,点击创建的按钮,即可拍照并保存到按钮对应的路径中。\n注意:在有的手机上,新建的文件夹需要重启以后才会被图库应用识别到。", 5 | "createANewSaver": "创建新的拍照键", 6 | "saverPath": "图片保存路径", 7 | "selectPath": "选择保存路径", 8 | "saverName": "拍照键名称", 9 | "saverNameDescription": "输入此拍照键的名称", 10 | "fileName": "文件名称", 11 | "ok": "确定", 12 | "cancel": "取消", 13 | "preview": "预览", 14 | "importAllExistingAlbums": "导入全部已有相册", 15 | "contactDeveloper": "联系开发者", 16 | "browseSourceCode": "浏览源码", 17 | "notYetImplemented": "尚未实现", 18 | "saverPathExisted": "❌已经存在这个路径的拍照键了", 19 | "thankForCharlie": "🎉感谢Charlie Sierra为本App提供的灵感!", 20 | "removeSaver": "删除拍照键?", 21 | "name": "名称", 22 | "path": "路径", 23 | "photoName": "照片名称", 24 | "photoNameDescription": "输入照片名称", 25 | "more": "更多", 26 | "photoIndex": "序号", 27 | "photoTimestamp": "时间", 28 | "photoNameExample": "照片名称示例", 29 | "moreDialogFinished": "照片名称设置完毕", 30 | "buy_me_coffee": "买杯咖啡给作者", 31 | "color_scheme": "颜色主题", 32 | "resolution_low": "低", 33 | "resolution_medium": "中", 34 | "resolution_high": "高", 35 | "resolution_vh": "超高", 36 | "resolution_uh": "极高", 37 | "resolution_max": "最大" 38 | } -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at https://dart.dev/lints. 17 | # 18 | # Instead of disabling a lint rule for the entire project in the 19 | # section below, it can also be suppressed for a single line of code 20 | # or a specific dart file by using the `// ignore: name_of_lint` and 21 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 22 | # producing the lint. 23 | rules: 24 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 25 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 26 | 27 | # Additional information about this file can be found at 28 | # https://dart.dev/guides/language/analysis-options 29 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '12.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | target 'RunnerTests' do 36 | inherit! :search_paths 37 | end 38 | end 39 | 40 | post_install do |installer| 41 | installer.pods_project.targets.each do |target| 42 | flutter_additional_ios_build_settings(target) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/l10n/app_en.arb: -------------------------------------------------------------------------------- 1 | { 2 | "appTitle": "SnapSaver", 3 | "howToUse": "How to use?", 4 | "helpContent": "First, click the button in the lower right corner, enter the path and name to save the photo, and create a photo shooting button; then, click the created button to take a photo and save it to the path corresponding to the button.\nNotice: In some devices, newly created path need a reboot to be recognized by album app.", 5 | "createANewSaver": "Create a new Saver", 6 | "saverPath": "Photo save path", 7 | "selectPath": "Select save path", 8 | "saverName": "Saver name", 9 | "saverNameDescription": "Input name of this saver", 10 | "fileName": "File name", 11 | "ok": "OK", 12 | "cancel": "Cancel", 13 | "preview": "Preview", 14 | "importAllExistingAlbums": "Import all existing albums", 15 | "contactDeveloper": "Contact author", 16 | "browseSourceCode": "Browse source code", 17 | "notYetImplemented": "Not yet implemented", 18 | "saverPathExisted": "❌Saver with this path already existed", 19 | "thankForCharlie": "🎉Thanks to Charlie Sierra for the inspiration for this app!", 20 | "removeSaver": "Remove Saver?", 21 | "name": "name", 22 | "path": "path", 23 | "photoName": "PhotoName", 24 | "photoNameDescription": "Input photo name", 25 | "more": "More", 26 | "photoIndex": "Index", 27 | "photoTimestamp": "Time", 28 | "photoNameExample": "PhotoName Example", 29 | "moreDialogFinished": "PhotoName set successfully", 30 | "buy_me_coffee": "Buy author a coffee", 31 | "color_scheme": "Color Scheme", 32 | "resolution_low": "low", 33 | "resolution_medium": "medium", 34 | "resolution_high": "high", 35 | "resolution_vh": "veryHigh", 36 | "resolution_uh": "ultraHigh", 37 | "resolution_max": "max" 38 | } -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Build Flutter APK 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build_apk: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/checkout@v4 15 | - name: set up JDK 17 16 | uses: actions/setup-java@v3 17 | with: 18 | java-version: '17' 19 | distribution: 'temurin' 20 | cache: gradle 21 | 22 | - name: Setup Flutter SDK 23 | uses: flutter-actions/setup-flutter@v3 24 | with: 25 | channel: stable 26 | version: 3.19.6 27 | 28 | - name: Install dependencies 29 | run: flutter pub get 30 | 31 | - name: Decode Keystore 32 | env: 33 | ENCODED_STRING: ${{ secrets.KEYSTORE_BASE_64 }} 34 | RELEASE_KEYSTORE_PASSWORD: ${{ secrets.RELEASE_KEYSTORE_PASSWORD }} 35 | RELEASE_KEYSTORE_ALIAS: ${{ secrets.RELEASE_KEYSTORE_ALIAS }} 36 | RELEASE_KEY_PASSWORD: ${{ secrets.RELEASE_KEY_PASSWORD }} 37 | 38 | run: | 39 | echo $ENCODED_STRING > keystore-b64.txt 40 | base64 -d keystore-b64.txt > fengfengkeystore.jks 41 | 42 | - name: Build Release apk 43 | env: 44 | RELEASE_KEYSTORE_PASSWORD: ${{ secrets.RELEASE_KEYSTORE_PASSWORD }} 45 | RELEASE_KEYSTORE_ALIAS: ${{ secrets.RELEASE_KEYSTORE_ALIAS }} 46 | RELEASE_KEY_PASSWORD: ${{ secrets.RELEASE_KEY_PASSWORD }} 47 | run: flutter build apk --release 48 | 49 | - name: Get release file apk path 50 | id: releaseApk 51 | run: echo "apkfile=$(find build/app/outputs/flutter-apk/*.apk)" >> $GITHUB_OUTPUT 52 | 53 | - name: Upload Release Build to Artifacts 54 | uses: actions/upload-artifact@v4 55 | with: 56 | name: release-artifacts 57 | path: ${{ steps.releaseApk.outputs.apkfile }} 58 | 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [中文](README.zh.md) 2 | # snap_saver 3 | 4 | A camera app that makes it easier to take photos for selected albums or directories. 5 | 6 |
7 | 2 8 | 2 9 | 3 10 |
11 | 12 | 13 | ## Getting Started 14 | 15 | [Get it on F-Droid](https://f-droid.org/packages/lying.fengfeng.snapsaver/) 18 | Or download the latest version from the [Release page](https://github.com/NielsLee/SnapSaver/releases/latest). 19 | 20 | 1. Launch the app and grant camera permission. 21 | 2. Press the floating button in the lower right corner and select the storage path and button name of the Saver button in the pop-up dialog box. 22 | 3. After entering the information, click OK, and the Saver button will remain on the homepage of the app. 23 | 4. Press the Saver button you just created, and the photo on the camera preview interface will be taken and saved to the path saved by the Saver button. 24 | 5. You can create as many Saver buttons as you want, assign different paths to them, and then easily shoot from the homepage of the app and save the results to different paths! 25 | 26 | ## Notes 27 | **On many Android devices, after capturing and storing a photo in a specified directory, it may not be immediately visible in the gallery. It could take some time or even a reboot before the photo appears in the gallery app. The author developed this software out of personal interest and cannot dedicate too much effort to solving this issue at the moment. If anyone reading this has a good solution, feel free to discuss it with the author via an Issue. Thank you very much!😊** 28 | 29 | Buy Me a Coffee at ko-fi.com 30 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | id "kotlin-android" 4 | id "dev.flutter.flutter-gradle-plugin" 5 | } 6 | 7 | def localProperties = new Properties() 8 | def localPropertiesFile = rootProject.file('local.properties') 9 | if (localPropertiesFile.exists()) { 10 | localPropertiesFile.withReader('UTF-8') { reader -> 11 | localProperties.load(reader) 12 | } 13 | } 14 | 15 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 16 | if (flutterVersionCode == null) { 17 | flutterVersionCode = '1' 18 | } 19 | 20 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 21 | if (flutterVersionName == null) { 22 | flutterVersionName = '1.0' 23 | } 24 | 25 | android { 26 | namespace "lying.fengfeng.snapsaver.snap_saver" 27 | compileSdkVersion 35 28 | ndkVersion flutter.ndkVersion 29 | 30 | compileOptions { 31 | sourceCompatibility JavaVersion.VERSION_1_8 32 | targetCompatibility JavaVersion.VERSION_1_8 33 | } 34 | 35 | kotlinOptions { 36 | jvmTarget = '1.8' 37 | freeCompilerArgs += ['-Xjvm-default=all'] 38 | } 39 | 40 | sourceSets { 41 | main.java.srcDirs += 'src/main/kotlin' 42 | } 43 | 44 | defaultConfig { 45 | applicationId "lying.fengfeng.snapsaver" 46 | minSdkVersion 21 47 | targetSdkVersion 34 48 | versionCode flutterVersionCode.toInteger() 49 | versionName flutterVersionName 50 | } 51 | 52 | signingConfigs { 53 | release{ 54 | storeFile file("../../fengfengkeystore.jks") 55 | storePassword System.getenv("RELEASE_KEYSTORE_PASSWORD") 56 | keyAlias System.getenv("RELEASE_KEYSTORE_ALIAS") 57 | keyPassword System.getenv("RELEASE_KEY_PASSWORD") 58 | } 59 | } 60 | 61 | buildTypes { 62 | release { 63 | signingConfig signingConfigs.release 64 | } 65 | } 66 | dependenciesInfo { 67 | includeInApk = false 68 | includeInBundle = false 69 | } 70 | } 71 | 72 | flutter { 73 | source '../..' 74 | } 75 | 76 | dependencies {} 77 | -------------------------------------------------------------------------------- /lib/dialog/remove_saver_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:snap_saver/entity/saver.dart'; 3 | import 'package:vibration/vibration.dart'; 4 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 5 | 6 | class RemoveSaverDialog extends StatelessWidget { 7 | Saver saver; 8 | RemoveSaverDialog({super.key, required this.saver}); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return AlertDialog( 13 | title: Row( 14 | children: [ 15 | Text(AppLocalizations.of(context)!.removeSaver), 16 | Spacer(), 17 | if (saver.color != null) 18 | Icon( 19 | Icons.folder, 20 | color: Color(saver.color!), 21 | ) 22 | ], 23 | ), 24 | content: Column( 25 | crossAxisAlignment: CrossAxisAlignment.start, 26 | mainAxisSize: MainAxisSize.min, 27 | children: [ 28 | Text(AppLocalizations.of(context)!.name + ":"), 29 | Text( 30 | saver.name, 31 | style: TextStyle( 32 | fontSize: 20, 33 | ), 34 | ), 35 | Padding(padding: EdgeInsets.all(8)), 36 | Text(AppLocalizations.of(context)!.path + ":"), 37 | Column( 38 | crossAxisAlignment: CrossAxisAlignment.start, 39 | mainAxisSize: MainAxisSize.min, 40 | children: saver.paths.map((path) { 41 | return Text(path); 42 | }).toList(), 43 | ) 44 | ], 45 | ), 46 | actions: [ 47 | // cancel button 48 | TextButton( 49 | child: Text(AppLocalizations.of(context)!.cancel), 50 | onPressed: () { 51 | Vibration.vibrate(amplitude: 255, duration: 5); 52 | Navigator.of(context).pop(); 53 | }, 54 | ), 55 | 56 | // ok button 57 | TextButton( 58 | child: Text(AppLocalizations.of(context)!.ok), 59 | onPressed: () { 60 | Vibration.vibrate(amplitude: 255, duration: 5); 61 | Navigator.of(context).pop(true); 62 | }, 63 | ), 64 | ], 65 | ); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSCameraUsageDescription 6 | Explanation on why the camera access is needed. 7 | NSPhotoLibraryAddUsageDescription 8 | Explanation on why the NSPhotoLibraryAddUsageDescription access is needed. 9 | NSPhotoLibraryUsageDescription 10 | 需要访问您的相册以选择照片 11 | NSMicrophoneUsageDescription 12 | Explanation on why the microphone access is needed. 13 | CFBundleDevelopmentRegion 14 | $(DEVELOPMENT_LANGUAGE) 15 | CFBundleDisplayName 16 | Snap Saver 17 | CFBundleExecutable 18 | $(EXECUTABLE_NAME) 19 | CFBundleIdentifier 20 | $(PRODUCT_BUNDLE_IDENTIFIER) 21 | CFBundleInfoDictionaryVersion 22 | 6.0 23 | CFBundleName 24 | snap_saver 25 | CFBundlePackageType 26 | APPL 27 | CFBundleShortVersionString 28 | $(FLUTTER_BUILD_NAME) 29 | CFBundleSignature 30 | ???? 31 | CFBundleVersion 32 | $(FLUTTER_BUILD_NUMBER) 33 | LSRequiresIPhoneOS 34 | 35 | UILaunchStoryboardName 36 | LaunchScreen 37 | UIMainStoryboardFile 38 | Main 39 | UISupportedInterfaceOrientations 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationLandscapeLeft 43 | UIInterfaceOrientationLandscapeRight 44 | 45 | UISupportedInterfaceOrientations~ipad 46 | 47 | UIInterfaceOrientationPortrait 48 | UIInterfaceOrientationPortraitUpsideDown 49 | UIInterfaceOrientationLandscapeLeft 50 | UIInterfaceOrientationLandscapeRight 51 | 52 | CADisableMinimumFrameDurationOnPhone 53 | 54 | UIApplicationSupportsIndirectInputEvents 55 | 56 | UIFileSharingEnabled 57 | 58 | NSDocumentsFolderUsageDescription 59 | NSDocumentsFolderUsageDescription 60 | NSFileProtectionCompleteUntilFirstUserAuthentication 61 | NSFileProtectionCompleteUntilFirstUserAuthentication 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 22 | 26 | 30 | 31 | 32 | 33 | 34 | 35 | 37 | 40 | 41 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /lib/viewmodel/home_view_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:snap_saver/db/SaverDatabase.dart'; 4 | import 'package:snap_saver/entity/saver.dart'; 5 | import 'package:shared_preferences/shared_preferences.dart'; 6 | 7 | class HomeViewModel extends ChangeNotifier { 8 | final List _saverList = []; 9 | Color _seedColor = Colors.green; 10 | int _resolution = 0; 11 | static const String _resolutionKey = 'resolution'; 12 | 13 | HomeViewModel() { 14 | _initSavers(); 15 | _loadSeedColor(); 16 | _loadResolution(); 17 | } 18 | 19 | List get savers => _saverList; 20 | Color get seedColor => _seedColor; 21 | int get resolution => _resolution; 22 | 23 | Future _loadSeedColor() async { 24 | final prefs = await SharedPreferences.getInstance(); 25 | final colorValue = prefs.getInt('seed_color') ?? Colors.green.value; 26 | _seedColor = Color(colorValue); 27 | notifyListeners(); 28 | } 29 | 30 | Future updateSeedColor(Color color) async { 31 | _seedColor = color; 32 | final prefs = await SharedPreferences.getInstance(); 33 | await prefs.setInt('seed_color', color.value); 34 | notifyListeners(); 35 | } 36 | 37 | Future _loadResolution() async { 38 | final prefs = await SharedPreferences.getInstance(); 39 | _resolution = prefs.getInt(_resolutionKey) ?? 2; // middle one 40 | notifyListeners(); 41 | } 42 | 43 | Future updateResolution(int value) async { 44 | if (value < 0 || value > 5) return; 45 | _resolution = value; 46 | final prefs = await SharedPreferences.getInstance(); 47 | await prefs.setInt(_resolutionKey, value); 48 | notifyListeners(); 49 | } 50 | 51 | int addSaver(Saver newSaver, BuildContext context) { 52 | if (_saverList.map((e) => e.name).contains(newSaver.name)) { 53 | return 0; 54 | } 55 | _saverList.add(newSaver); 56 | SaverDatabase().insertSaver(newSaver); 57 | notifyListeners(); 58 | return 1; 59 | } 60 | 61 | void removeSaver(Saver saver) { 62 | _saverList.remove(saver); 63 | SaverDatabase().deleteSaver(saver); 64 | notifyListeners(); 65 | } 66 | 67 | void _initSavers() async { 68 | final existedSavers = await SaverDatabase().getAllSavers(); 69 | _saverList.addAll(existedSavers); 70 | notifyListeners(); 71 | } 72 | 73 | int updateSaver(Saver updatedSaver) { 74 | final index = 75 | _saverList.indexWhere((saver) => saver.name == updatedSaver.name); 76 | 77 | if (index != -1) { 78 | _saverList[index] = updatedSaver; 79 | 80 | SaverDatabase().updateSaver(updatedSaver); 81 | notifyListeners(); 82 | return 1; // success 83 | } else { 84 | return 0; 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /assets/icons/snapsaver_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | 12 | 16 | 24 | 26 | 29 | 31 | 33 | 35 | 37 | 39 | 41 | 43 | 45 | 48 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: snap_saver 2 | description: "A camera app that makes it easier to take photos for selected albums or directories." 3 | # The following line prevents the package from being accidentally published to 4 | # pub.dev using `flutter pub publish`. This is preferred for private packages. 5 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 6 | 7 | # The following defines the version and build number for your application. 8 | # A version number is three numbers separated by dots, like 1.2.43 9 | # followed by an optional build number separated by a +. 10 | # Both the version and the builder number may be overridden in flutter 11 | # build by specifying --build-name and --build-number, respectively. 12 | # In Android, build-name is used as versionName while build-number used as versionCode. 13 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 14 | # In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. 15 | # Read more about iOS versioning at 16 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 17 | # In Windows, build-name is used as the major, minor, and patch parts 18 | # of the product and file versions while build-number is used as the build suffix. 19 | version: 0.9.0+10 20 | 21 | environment: 22 | sdk: '>=3.3.4 <4.0.0' 23 | 24 | # Dependencies specify other packages that your package needs in order to work. 25 | # To automatically upgrade your package dependencies to the latest versions 26 | # consider running `flutter pub upgrade --major-versions`. Alternatively, 27 | # dependencies can be manually updated by changing the version numbers below to 28 | # the latest version available on pub.dev. To see which dependencies have newer 29 | # versions available, run `flutter pub outdated`. 30 | dependencies: 31 | flutter: 32 | sdk: flutter 33 | shared_preferences: ^2.2.2 34 | 35 | # The following adds the Cupertino Icons font to your application. 36 | # Use with the CupertinoIcons class for iOS style icons. 37 | cupertino_icons: ^1.0.6 38 | camera: ^0.11.0+1 39 | path_provider: ^2.1.3 40 | path: ^1.9.0 41 | dynamic_color: ^1.7.0 42 | flutter_staggered_grid_view: ^0.7.0 43 | provider: ^6.1.2 44 | file_picker: ^8.0.5 45 | sqflite: ^2.3.3+1 46 | audioplayers: ^6.0.0 47 | url_launcher: ^6.3.0 48 | flutter_localization: ^0.2.0 49 | flutter_launcher_icons: ^0.13.1 50 | vibration: ^1.8.3 51 | intl: any 52 | fluttertoast: ^8.2.5 53 | permission_handler: ^11.3.1 54 | image: ^4.2.0 55 | 56 | dev_dependencies: 57 | flutter_test: 58 | sdk: flutter 59 | flutter_localizations: 60 | sdk: flutter 61 | 62 | flutter_launcher_icons: 63 | android: "launcher_icon" 64 | ios: true 65 | image_path: "assets/icons/snapsaver_icon.png" 66 | 67 | 68 | # The "flutter_lints" package below contains a set of recommended lints to 69 | # encourage good coding practices. The lint set provided by the package is 70 | # activated in the `analysis_options.yaml` file located at the root of your 71 | # package. See that file for information about deactivating specific lint 72 | # rules and activating additional ones. 73 | flutter_lints: ^3.0.0 74 | 75 | # For information on the generic Dart part of this file, see the 76 | # following page: https://dart.dev/tools/pub/pubspec 77 | 78 | # The following section is specific to Flutter packages. 79 | flutter: 80 | assets: 81 | - assets/sounds/camera_shutter.mp3 82 | - assets/icons/github_mark.png 83 | - assets/icons/snapsaver_icon.svg 84 | generate: true 85 | 86 | # The following line ensures that the Material Icons font is 87 | # included with your application, so that you can use the icons in 88 | # the material Icons class. 89 | uses-material-design: true 90 | 91 | # To add assets to your application, add an assets section, like this: 92 | # assets: 93 | # - images/a_dot_burr.jpeg 94 | # - images/a_dot_ham.jpeg 95 | 96 | # An image asset can refer to one or more resolution-specific "variants", see 97 | # https://flutter.dev/assets-and-images/#resolution-aware 98 | 99 | # For details regarding adding assets from package dependencies, see 100 | # https://flutter.dev/assets-and-images/#from-packages 101 | 102 | # To add custom fonts to your application, add a fonts section here, 103 | # in this "flutter" section. Each entry in this list should have a 104 | # "family" key with the font family name, and a "fonts" key with a 105 | # list giving the asset and other descriptors for the font. For 106 | # example: 107 | # fonts: 108 | # - family: Schyler 109 | # fonts: 110 | # - asset: fonts/Schyler-Regular.ttf 111 | # - asset: fonts/Schyler-Italic.ttf 112 | # style: italic 113 | # - family: Trajan Pro 114 | # fonts: 115 | # - asset: fonts/TrajanPro.ttf 116 | # - asset: fonts/TrajanPro_Bold.ttf 117 | # weight: 700 118 | # 119 | # For details regarding fonts from package dependencies, 120 | # see https://flutter.dev/custom-fonts/#from-packages 121 | -------------------------------------------------------------------------------- /ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - audioplayers_darwin (0.0.1): 3 | - Flutter 4 | - camera_avfoundation (0.0.1): 5 | - Flutter 6 | - device_info_plus (0.0.1): 7 | - Flutter 8 | - DKImagePickerController/Core (4.3.9): 9 | - DKImagePickerController/ImageDataManager 10 | - DKImagePickerController/Resource 11 | - DKImagePickerController/ImageDataManager (4.3.9) 12 | - DKImagePickerController/PhotoGallery (4.3.9): 13 | - DKImagePickerController/Core 14 | - DKPhotoGallery 15 | - DKImagePickerController/Resource (4.3.9) 16 | - DKPhotoGallery (0.0.19): 17 | - DKPhotoGallery/Core (= 0.0.19) 18 | - DKPhotoGallery/Model (= 0.0.19) 19 | - DKPhotoGallery/Preview (= 0.0.19) 20 | - DKPhotoGallery/Resource (= 0.0.19) 21 | - SDWebImage 22 | - SwiftyGif 23 | - DKPhotoGallery/Core (0.0.19): 24 | - DKPhotoGallery/Model 25 | - DKPhotoGallery/Preview 26 | - SDWebImage 27 | - SwiftyGif 28 | - DKPhotoGallery/Model (0.0.19): 29 | - SDWebImage 30 | - SwiftyGif 31 | - DKPhotoGallery/Preview (0.0.19): 32 | - DKPhotoGallery/Model 33 | - DKPhotoGallery/Resource 34 | - SDWebImage 35 | - SwiftyGif 36 | - DKPhotoGallery/Resource (0.0.19): 37 | - SDWebImage 38 | - SwiftyGif 39 | - file_picker (0.0.1): 40 | - DKImagePickerController/PhotoGallery 41 | - Flutter 42 | - Flutter (1.0.0) 43 | - flutter_localization (0.0.1): 44 | - Flutter 45 | - fluttertoast (0.0.2): 46 | - Flutter 47 | - Toast 48 | - path_provider_foundation (0.0.1): 49 | - Flutter 50 | - FlutterMacOS 51 | - permission_handler_apple (9.3.0): 52 | - Flutter 53 | - SDWebImage (5.21.3): 54 | - SDWebImage/Core (= 5.21.3) 55 | - SDWebImage/Core (5.21.3) 56 | - shared_preferences_foundation (0.0.1): 57 | - Flutter 58 | - FlutterMacOS 59 | - sqflite (0.0.3): 60 | - Flutter 61 | - FlutterMacOS 62 | - SwiftyGif (5.4.5) 63 | - Toast (4.1.1) 64 | - url_launcher_ios (0.0.1): 65 | - Flutter 66 | - vibration (1.7.5): 67 | - Flutter 68 | 69 | DEPENDENCIES: 70 | - audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/ios`) 71 | - camera_avfoundation (from `.symlinks/plugins/camera_avfoundation/ios`) 72 | - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) 73 | - file_picker (from `.symlinks/plugins/file_picker/ios`) 74 | - Flutter (from `Flutter`) 75 | - flutter_localization (from `.symlinks/plugins/flutter_localization/ios`) 76 | - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) 77 | - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) 78 | - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) 79 | - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) 80 | - sqflite (from `.symlinks/plugins/sqflite/darwin`) 81 | - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) 82 | - vibration (from `.symlinks/plugins/vibration/ios`) 83 | 84 | SPEC REPOS: 85 | trunk: 86 | - DKImagePickerController 87 | - DKPhotoGallery 88 | - SDWebImage 89 | - SwiftyGif 90 | - Toast 91 | 92 | EXTERNAL SOURCES: 93 | audioplayers_darwin: 94 | :path: ".symlinks/plugins/audioplayers_darwin/ios" 95 | camera_avfoundation: 96 | :path: ".symlinks/plugins/camera_avfoundation/ios" 97 | device_info_plus: 98 | :path: ".symlinks/plugins/device_info_plus/ios" 99 | file_picker: 100 | :path: ".symlinks/plugins/file_picker/ios" 101 | Flutter: 102 | :path: Flutter 103 | flutter_localization: 104 | :path: ".symlinks/plugins/flutter_localization/ios" 105 | fluttertoast: 106 | :path: ".symlinks/plugins/fluttertoast/ios" 107 | path_provider_foundation: 108 | :path: ".symlinks/plugins/path_provider_foundation/darwin" 109 | permission_handler_apple: 110 | :path: ".symlinks/plugins/permission_handler_apple/ios" 111 | shared_preferences_foundation: 112 | :path: ".symlinks/plugins/shared_preferences_foundation/darwin" 113 | sqflite: 114 | :path: ".symlinks/plugins/sqflite/darwin" 115 | url_launcher_ios: 116 | :path: ".symlinks/plugins/url_launcher_ios/ios" 117 | vibration: 118 | :path: ".symlinks/plugins/vibration/ios" 119 | 120 | SPEC CHECKSUMS: 121 | audioplayers_darwin: 877d9a4d06331c5c374595e46e16453ac7eafa40 122 | camera_avfoundation: dd002b0330f4981e1bbcb46ae9b62829237459a4 123 | device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d 124 | DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c 125 | DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 126 | file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 127 | Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 128 | flutter_localization: f43b18844a2b3d2c71fd64f04ffd6b1e64dd54d4 129 | fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c 130 | path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 131 | permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 132 | SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a 133 | shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 134 | sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec 135 | SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 136 | Toast: 1f5ea13423a1e6674c4abdac5be53587ae481c4e 137 | url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe 138 | vibration: 7d883d141656a1c1a6d8d238616b2042a51a1241 139 | 140 | PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796 141 | 142 | COCOAPODS: 1.15.2 143 | -------------------------------------------------------------------------------- /lib/dialog/more_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 5 | import 'package:intl/intl.dart'; 6 | import 'package:snap_saver/entity/more.dart'; 7 | 8 | class MoreDialog extends StatefulWidget { 9 | const MoreDialog({super.key}); 10 | 11 | @override 12 | State createState() => MoreDialogState(); 13 | } 14 | 15 | class MoreDialogState extends State { 16 | TextEditingController fileNameController = TextEditingController(); 17 | String examplePhotoName = ""; 18 | 19 | int _selectedIndex = 0; 20 | 21 | void _refreshExamplePhotoName() { 22 | String newValue = fileNameController.text; 23 | switch (_selectedIndex) { 24 | case 0: 25 | { 26 | examplePhotoName = newValue + "1" + '\n' + newValue + "2"; 27 | break; 28 | } 29 | case 1: 30 | { 31 | DateTime now = DateTime.now(); 32 | DateTime yesterday = now.subtract(const Duration(days: 1)); 33 | 34 | String nowStr = DateFormat('yyyyMMddHHmmss').format(now); 35 | String yesterdayStr = DateFormat('yyyyMMddHHmmss').format(yesterday); 36 | examplePhotoName = newValue + nowStr + '\n' + newValue + yesterdayStr; 37 | break; 38 | } 39 | case 2: 40 | { 41 | examplePhotoName = newValue + "_1" + '\n' + newValue + "_2"; 42 | break; 43 | } 44 | case 3: 45 | { 46 | DateTime now = DateTime.now(); 47 | DateTime yesterday = now.subtract(const Duration(days: 1)); 48 | 49 | String nowStr = DateFormat('yyyyMMddHHmmss').format(now); 50 | String yesterdayStr = DateFormat('yyyyMMddHHmmss').format(yesterday); 51 | examplePhotoName = 52 | newValue + '_' + nowStr + '\n' + newValue + '_' + yesterdayStr; 53 | break; 54 | } 55 | case 4: 56 | { 57 | examplePhotoName = newValue + "-1" + '\n' + newValue + "-2"; 58 | break; 59 | } 60 | case 5: 61 | { 62 | DateTime now = DateTime.now(); 63 | DateTime yesterday = now.subtract(const Duration(days: 1)); 64 | 65 | String nowStr = DateFormat('yyyyMMddHHmmss').format(now); 66 | String yesterdayStr = DateFormat('yyyyMMddHHmmss').format(yesterday); 67 | examplePhotoName = 68 | newValue + '-' + nowStr + '\n' + newValue + '-' + yesterdayStr; 69 | break; 70 | } 71 | } 72 | } 73 | 74 | @override 75 | Widget build(BuildContext context) { 76 | final List _typeList = [ 77 | AppLocalizations.of(context)!.photoIndex, 78 | AppLocalizations.of(context)!.photoTimestamp, 79 | "_" + AppLocalizations.of(context)!.photoIndex, 80 | "_" + AppLocalizations.of(context)!.photoTimestamp, 81 | "-" + AppLocalizations.of(context)!.photoIndex, 82 | "-" + AppLocalizations.of(context)!.photoTimestamp, 83 | ]; 84 | 85 | return AlertDialog( 86 | title: Text(AppLocalizations.of(context)!.more), 87 | content: Column( 88 | mainAxisSize: MainAxisSize.min, 89 | crossAxisAlignment: CrossAxisAlignment.start, 90 | children: [ 91 | Text(AppLocalizations.of(context)!.photoName), 92 | Row( 93 | children: [ 94 | Expanded( 95 | flex: 1, 96 | child: TextField( 97 | controller: fileNameController, 98 | onTap: () {}, 99 | onChanged: (newValue) { 100 | setState(() { 101 | _refreshExamplePhotoName(); 102 | }); 103 | }, 104 | decoration: InputDecoration( 105 | label: Text( 106 | AppLocalizations.of(context)!.photoNameDescription), 107 | floatingLabelBehavior: FloatingLabelBehavior.never, 108 | border: const OutlineInputBorder( 109 | borderRadius: 110 | BorderRadius.all(Radius.circular(12)))), 111 | )), 112 | Text(" + "), 113 | DropdownButton( 114 | value: _typeList[_selectedIndex], 115 | items: _typeList.map((String item) { 116 | return DropdownMenuItem( 117 | value: item, 118 | child: Text(item), 119 | ); 120 | }).toList(), 121 | onChanged: (String? newValue) { 122 | setState(() { 123 | if (newValue != null) { 124 | _selectedIndex = _typeList.indexOf(newValue); 125 | } 126 | _refreshExamplePhotoName(); 127 | }); 128 | }, 129 | ) 130 | ], 131 | ), 132 | Text(AppLocalizations.of(context)!.photoNameExample + ':'), 133 | Text(examplePhotoName) 134 | ], 135 | ), 136 | actions: [ 137 | TextButton( 138 | onPressed: () { 139 | Navigator.of(context).pop(); 140 | }, 141 | child: Text(AppLocalizations.of(context)!.cancel)), 142 | TextButton( 143 | onPressed: () { 144 | var name = fileNameController.text; 145 | if (name.isEmpty) { 146 | // no name input, use default 147 | Navigator.of(context).pop(); 148 | return; 149 | } 150 | var more = More(photoName: name, suffixType: _selectedIndex); 151 | Navigator.of(context).pop(more); 152 | }, 153 | child: Text(AppLocalizations.of(context)!.ok)), 154 | ], 155 | ); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /lib/db/SaverDatabase.dart: -------------------------------------------------------------------------------- 1 | import 'package:path/path.dart'; 2 | import 'package:snap_saver/constants.dart'; 3 | import 'package:sqflite/sqflite.dart'; 4 | 5 | import '../entity/saver.dart'; 6 | 7 | class SaverDatabase { 8 | late Database _database; 9 | bool _isInitialized = false; 10 | 11 | Future get database async { 12 | if (_isInitialized) return _database; 13 | 14 | _database = await _init(); 15 | _isInitialized = true; 16 | return _database; 17 | } 18 | 19 | Future insertSaver(Saver saver) async { 20 | final db = await database; 21 | 22 | final saverMap = { 23 | 'path': null, 24 | 'name': saver.name, 25 | 'color': saver.color, 26 | 'count': saver.count, 27 | 'photoName': saver.photoName, 28 | 'suffixType': saver.suffixType 29 | }; 30 | // insert saver to saver table 31 | db.insert(Constants.saverTableName, saverMap, 32 | conflictAlgorithm: ConflictAlgorithm.rollback); 33 | 34 | // insert paths to path table 35 | final saverName = saver.name; 36 | final pathList = saver.paths; 37 | for (String path in pathList) { 38 | db.insert( 39 | Constants.pathTableName, 40 | { 41 | "saver_name": saverName, 42 | "path": path, 43 | }, 44 | conflictAlgorithm: ConflictAlgorithm.rollback); 45 | } 46 | return; 47 | } 48 | 49 | Future deleteSaver(Saver saver) async { 50 | final db = await database; 51 | 52 | db.delete(Constants.saverTableName, 53 | where: 'name = ?', whereArgs: [saver.name]); 54 | 55 | db.delete(Constants.pathTableName, 56 | where: 'saver_name = ?', whereArgs: [saver.name]); 57 | } 58 | 59 | Future> getAllSavers() async { 60 | final db = await database; 61 | List resultList = []; 62 | 63 | final List> savers = 64 | await db.query(Constants.saverTableName); 65 | 66 | for (Map saverMap in savers) { 67 | String saverName = saverMap['name']; 68 | int? saverColor = saverMap['color']; 69 | int count = saverMap['count']; 70 | String? photoName = saverMap['photoName']; 71 | int suffixType = saverMap['suffixType']; 72 | 73 | List pathList = []; 74 | List> paths = await db.query(Constants.pathTableName, 75 | where: 'saver_name = ?', whereArgs: [saverName]); 76 | for (Map pathMap in paths) { 77 | pathList.add(pathMap['path']); 78 | } 79 | 80 | resultList.add(Saver( 81 | paths: pathList, 82 | name: saverName, 83 | color: saverColor, 84 | count: count, 85 | photoName: photoName, 86 | suffixType: suffixType)); 87 | } 88 | 89 | return resultList; 90 | } 91 | 92 | Future updateSaver(Saver saver) async { 93 | final db = await database; 94 | 95 | final saverMap = { 96 | 'path': null, 97 | 'name': saver.name, 98 | 'color': saver.color, 99 | 'count': saver.count, 100 | 'photoName': saver.photoName, 101 | 'suffixType': saver.suffixType 102 | }; 103 | 104 | return await db.update( 105 | Constants.saverTableName, 106 | saverMap, 107 | where: 'name = ?', // 条件 108 | whereArgs: [saver.name], // 条件参数 109 | ); 110 | } 111 | 112 | Future _init() async { 113 | return openDatabase( 114 | join(await getDatabasesPath(), Constants.dbName), 115 | version: Constants.dbVersion, 116 | onCreate: (db, version) { 117 | db.execute( 118 | ''' 119 | CREATE TABLE IF NOT EXISTS ${Constants.saverTableName}( 120 | path TEXT PRIMARY KEY, 121 | name TEXT, 122 | color INTEGER DEFAULT NULL, 123 | count INTEGER, 124 | photoName TEXT DEFAULT NULL, 125 | suffixType INTEGER 126 | ) 127 | ''', 128 | ); 129 | 130 | db.execute(''' 131 | CREATE TABLE IF NOT EXISTS ${Constants.pathTableName}( 132 | id INTEGER PRIMARY KEY AUTOINCREMENT, 133 | saver_name TEXT, 134 | path TEXT, 135 | FOREIGN KEY(saver_name) REFERENCES ${Constants.saverTableName}(name) 136 | ) 137 | '''); 138 | 139 | return; 140 | }, 141 | onUpgrade: _onUpgrade, 142 | onDowngrade: (db, oldVersion, newVersion) {}, 143 | ); 144 | } 145 | 146 | Future _onUpgrade(Database db, int oldVersion, int newVersion) async { 147 | if (oldVersion == 1) { 148 | await _1_2(db); 149 | await _2_3(db); 150 | } else if (oldVersion == 2) { 151 | await _2_3(db); 152 | } 153 | } 154 | 155 | Future _1_2(Database db) async { 156 | await db.execute(''' 157 | CREATE TABLE IF NOT EXISTS tmp_table( 158 | path TEXT, 159 | name TEXT PRIMARY KEY, 160 | color INTEGER DEFAULT NULL 161 | ) 162 | '''); 163 | 164 | await db.execute(''' 165 | INSERT INTO tmp_table (path, name) 166 | SELECT path, name FROM ${Constants.saverTableName} 167 | '''); 168 | 169 | await db.execute('DROP TABLE IF EXISTS ${Constants.saverTableName}'); 170 | 171 | await db 172 | .execute('ALTER TABLE tmp_table RENAME TO ${Constants.saverTableName}'); 173 | 174 | await db.execute(''' 175 | CREATE TABLE IF NOT EXISTS ${Constants.pathTableName}( 176 | id INTEGER PRIMARY KEY AUTOINCREMENT, 177 | saver_name TEXT, 178 | path TEXT, 179 | FOREIGN KEY(saver_name) REFERENCES ${Constants.saverTableName}(name) 180 | ) 181 | '''); 182 | 183 | final List> existingData = 184 | await db.query(Constants.saverTableName); 185 | for (var row in existingData) { 186 | await db.insert( 187 | 'saver_paths', 188 | {'saver_name': row['name'], 'path': row['path']}, 189 | ); 190 | } 191 | } 192 | 193 | Future _2_3(Database db) async { 194 | await db.execute(''' 195 | ALTER TABLE ${Constants.saverTableName} ADD COLUMN count INTEGER DEFAULT 0 196 | '''); 197 | 198 | await db.execute(''' 199 | ALTER TABLE ${Constants.saverTableName} ADD COLUMN photoName TEXT DEFAULT NULL 200 | '''); 201 | 202 | await db.execute(''' 203 | ALTER TABLE ${Constants.saverTableName} ADD COLUMN suffixType INTEGER DEFAULT 0 204 | '''); 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/services.dart'; 4 | import 'package:provider/provider.dart'; 5 | import 'package:snap_saver/dialog/help_dialog.dart'; 6 | import 'package:snap_saver/dialog/insert_saver_dialog.dart'; 7 | import 'package:snap_saver/settings_screen.dart'; 8 | import 'package:snap_saver/viewmodel/dialog_view_model.dart'; 9 | import 'package:snap_saver/viewmodel/home_view_model.dart'; 10 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 11 | import 'package:vibration/vibration.dart'; 12 | import 'entity/saver.dart'; 13 | import 'home_screen.dart'; 14 | 15 | Future main() async { 16 | WidgetsFlutterBinding.ensureInitialized(); 17 | SystemChrome.setSystemUIOverlayStyle( 18 | const SystemUiOverlayStyle(systemNavigationBarColor: Colors.transparent)); 19 | SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); 20 | 21 | // Only support protrait layout now 22 | // TODO: support landscape layout 23 | SystemChrome.setPreferredOrientations([ 24 | DeviceOrientation.portraitUp, 25 | DeviceOrientation.portraitDown, 26 | ]); 27 | 28 | runApp( 29 | MultiProvider( 30 | providers: [ 31 | ChangeNotifierProvider(create: (_) => HomeViewModel()), 32 | ], 33 | child: const MainApp(), 34 | ), 35 | ); 36 | } 37 | 38 | class MainApp extends StatefulWidget { 39 | const MainApp({super.key}); 40 | 41 | @override 42 | State createState() => MainAppState(); 43 | } 44 | 45 | class MainAppState extends State { 46 | @override 47 | Widget build(BuildContext context) { 48 | return MaterialApp( 49 | localizationsDelegates: AppLocalizations.localizationsDelegates, 50 | supportedLocales: AppLocalizations.supportedLocales, 51 | theme: ThemeData( 52 | colorScheme: ColorScheme.fromSeed( 53 | seedColor: context.watch().seedColor, 54 | ), 55 | ), 56 | darkTheme: ThemeData( 57 | colorScheme: ColorScheme.fromSeed( 58 | seedColor: context.watch().seedColor, 59 | ), 60 | ), 61 | home: const MainScaffold(), 62 | ); 63 | } 64 | } 65 | 66 | class MainScaffold extends StatefulWidget { 67 | const MainScaffold({super.key}); 68 | 69 | @override 70 | State createState() => MainScaffoldState(); 71 | } 72 | 73 | class MainScaffoldState extends State { 74 | final List _screens = [ 75 | const HomeScreen(), 76 | const SettingsScreen(), 77 | ]; 78 | int _selectedIndex = 0; 79 | 80 | @override 81 | Widget build(BuildContext context) { 82 | return Consumer(builder: (_, homeViewModel, __) { 83 | return Scaffold( 84 | appBar: AppBar( 85 | title: Text(AppLocalizations.of(context)!.appTitle), 86 | backgroundColor: Theme.of(context).colorScheme.primaryContainer, 87 | actions: [ 88 | DropdownButton( 89 | value: context.watch().resolution, 90 | onChanged: (int? newResolution) { 91 | if (newResolution == null) return; 92 | Vibration.vibrate(amplitude: 255, duration: 5); 93 | context.read().updateResolution(newResolution); 94 | }, 95 | underline: Divider(height: 0, color: Colors.transparent), 96 | items: [ 97 | DropdownMenuItem( 98 | value: 0, 99 | child: Text(AppLocalizations.of(context)!.resolution_low)), 100 | DropdownMenuItem( 101 | value: 1, 102 | child: 103 | Text(AppLocalizations.of(context)!.resolution_medium)), 104 | DropdownMenuItem( 105 | value: 2, 106 | child: Text(AppLocalizations.of(context)!.resolution_high)), 107 | DropdownMenuItem( 108 | value: 3, 109 | child: Text(AppLocalizations.of(context)!.resolution_vh)), 110 | DropdownMenuItem( 111 | value: 4, 112 | child: Text(AppLocalizations.of(context)!.resolution_uh)), 113 | DropdownMenuItem( 114 | value: 5, 115 | child: Text(AppLocalizations.of(context)!.resolution_max)), 116 | ], 117 | icon: Container( 118 | padding: EdgeInsets.all(8), 119 | child: Icon(Icons.settings_overscan), 120 | ), 121 | ), 122 | IconButton( 123 | onPressed: () { 124 | Vibration.vibrate(amplitude: 255, duration: 5); 125 | _showHelpDialog(); 126 | }, 127 | icon: Icon(Icons.help_outline)), 128 | Padding(padding: EdgeInsets.fromLTRB(0, 0, 8, 0)) 129 | ], 130 | ), 131 | body: _screens[_selectedIndex], 132 | floatingActionButton: FloatingActionButton( 133 | onPressed: () async { 134 | final dialogViewModel = await _showInsertDialog(); 135 | if (dialogViewModel != null) { 136 | final newSaver = Saver( 137 | paths: dialogViewModel.getPath(), 138 | name: dialogViewModel.getName(), 139 | color: dialogViewModel.getColor()?.value, 140 | photoName: dialogViewModel.getPhotoName(), 141 | suffixType: dialogViewModel.getSuffixType()); 142 | 143 | int res = homeViewModel.addSaver(newSaver, context); 144 | if (res == 0) { 145 | final snackBar = SnackBar( 146 | content: Text(AppLocalizations.of(context)!.saverPathExisted), 147 | ); 148 | ScaffoldMessenger.of(context).showSnackBar(snackBar); 149 | } 150 | } 151 | }, 152 | child: const Icon(Icons.add), 153 | ), 154 | floatingActionButtonLocation: FloatingActionButtonLocation.endContained, 155 | bottomNavigationBar: BottomAppBar( 156 | child: Row( 157 | mainAxisAlignment: MainAxisAlignment.start, 158 | children: [ 159 | IconButton( 160 | icon: const Icon(Icons.home), 161 | onPressed: () { 162 | Vibration.vibrate(amplitude: 255, duration: 5); 163 | setState(() { 164 | _selectedIndex = 0; 165 | }); 166 | }, 167 | ), 168 | IconButton( 169 | icon: const Icon(Icons.settings), 170 | onPressed: () { 171 | Vibration.vibrate(amplitude: 255, duration: 5); 172 | setState(() { 173 | _selectedIndex = 1; 174 | }); 175 | }, 176 | ), 177 | ], 178 | ), 179 | ), 180 | ); 181 | }); 182 | } 183 | 184 | Future _showInsertDialog() async { 185 | Vibration.vibrate(amplitude: 255, duration: 5); 186 | return showGeneralDialog( 187 | context: context, 188 | barrierDismissible: false, 189 | pageBuilder: (BuildContext context, anim1, anmi2) { 190 | return const InsertSaverDialog(); 191 | }, 192 | ); 193 | } 194 | 195 | Future _showHelpDialog() async { 196 | return showGeneralDialog( 197 | context: context, 198 | barrierDismissible: true, 199 | barrierLabel: "Help Dialog", 200 | pageBuilder: (BuildContext context, anim1, anmi2) { 201 | return HelpDialog(); 202 | }, 203 | ); 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /lib/settings_screen.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:url_launcher/url_launcher.dart'; 5 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 6 | import 'package:fluttertoast/fluttertoast.dart'; 7 | import 'package:provider/provider.dart'; 8 | import 'package:vibration/vibration.dart'; 9 | import 'package:snap_saver/viewmodel/home_view_model.dart'; 10 | 11 | class SettingsScreen extends StatefulWidget { 12 | const SettingsScreen({super.key}); 13 | 14 | @override 15 | State createState() => SettingsScreenState(); 16 | } 17 | 18 | class SettingsScreenState extends State { 19 | bool isAddButtonShown = true; 20 | bool isColorMenuExpanded = false; 21 | 22 | @override 23 | Widget build(BuildContext context) { 24 | return ListView( 25 | children: [ 26 | Divider(height: 0), 27 | Container( 28 | padding: const EdgeInsets.all(8), 29 | child: Row( 30 | children: [ 31 | Padding(padding: EdgeInsets.fromLTRB(16, 0, 0, 0)), 32 | Text(AppLocalizations.of(context)!.contactDeveloper, 33 | style: TextStyle(fontWeight: FontWeight.bold)), 34 | const Spacer(), 35 | IconButton( 36 | onPressed: _showThanks, icon: const Icon(Icons.thumb_up)), 37 | IconButton(onPressed: _launchMail, icon: const Icon(Icons.mail)) 38 | ], 39 | ), 40 | ), 41 | Divider(height: 0), 42 | Container( 43 | padding: const EdgeInsets.all(8), 44 | child: Row( 45 | children: [ 46 | Padding(padding: EdgeInsets.fromLTRB(16, 0, 0, 0)), 47 | Text(AppLocalizations.of(context)!.browseSourceCode, 48 | style: TextStyle(fontWeight: FontWeight.bold)), 49 | const Spacer(), 50 | IconButton( 51 | onPressed: () { 52 | _launchGithub(); 53 | }, 54 | icon: const ImageIcon( 55 | AssetImage('assets/icons/github_mark.png'), 56 | size: 24.0, 57 | )) 58 | ], 59 | ), 60 | ), 61 | Divider(height: 0), 62 | Container( 63 | padding: const EdgeInsets.all(8), 64 | child: Row( 65 | children: [ 66 | Padding(padding: EdgeInsets.fromLTRB(16, 0, 0, 0)), 67 | Text(AppLocalizations.of(context)!.buy_me_coffee, 68 | style: TextStyle(fontWeight: FontWeight.bold)), 69 | const Spacer(), 70 | IconButton( 71 | onPressed: () { 72 | _launchCoffee(); 73 | }, 74 | icon: const Icon(Icons.coffee)) 75 | ], 76 | ), 77 | ), 78 | Divider(height: 0), 79 | Column( 80 | children: [ 81 | Container( 82 | padding: const EdgeInsets.all(8), 83 | child: Row( 84 | children: [ 85 | Padding(padding: EdgeInsets.fromLTRB(16, 0, 0, 0)), 86 | Text(AppLocalizations.of(context)!.color_scheme, 87 | style: TextStyle(fontWeight: FontWeight.bold)), 88 | const Spacer(), 89 | IconButton( 90 | onPressed: () { 91 | setState(() { 92 | isColorMenuExpanded = !isColorMenuExpanded; 93 | }); 94 | }, 95 | icon: Icon(isColorMenuExpanded 96 | ? Icons.expand_less 97 | : Icons.expand_more)) 98 | ], 99 | ), 100 | ), 101 | if (isColorMenuExpanded) 102 | Container( 103 | padding: const EdgeInsets.all(8), 104 | child: Row( 105 | children: [ 106 | Padding(padding: EdgeInsets.all(24)), 107 | _buildColorButton(Colors.red), 108 | _buildColorButton(Colors.orange), 109 | _buildColorButton(Colors.yellow), 110 | _buildColorButton(Colors.green), 111 | _buildColorButton(Colors.cyan), 112 | _buildColorButton(Colors.blue), 113 | _buildColorButton(Colors.purple), 114 | ], 115 | ), 116 | ), 117 | ], 118 | ), 119 | Divider(height: 0), 120 | ], 121 | ); 122 | } 123 | 124 | void _showThanks() { 125 | ScaffoldMessenger.of(context).showSnackBar( 126 | SnackBar( 127 | content: Text(AppLocalizations.of(context)!.thankForCharlie), 128 | duration: Duration(seconds: 2), 129 | ), 130 | ); 131 | } 132 | 133 | Future _launchMail() async { 134 | final Uri emailUrl = Uri( 135 | scheme: 'mailto', 136 | path: 'Niels_Lee@outlook.com', 137 | queryParameters: { 138 | 'subject': 'SnapSaver', 139 | }, 140 | ); 141 | 142 | if (!await launchUrl(emailUrl)) { 143 | Fluttertoast.showToast( 144 | msg: '☹️Failed to launch email', 145 | toastLength: Toast.LENGTH_SHORT, 146 | gravity: ToastGravity.BOTTOM, 147 | backgroundColor: Colors.white, 148 | textColor: Colors.black, 149 | ); 150 | } 151 | } 152 | 153 | Future _launchGithub() async { 154 | final Uri githubUrl = Uri( 155 | scheme: 'https', 156 | path: 'github.com/NielsLee/SnapSaver', 157 | ); 158 | 159 | if (!await launchUrl(githubUrl)) { 160 | Fluttertoast.showToast( 161 | msg: '☹️Failed to launch Github', 162 | toastLength: Toast.LENGTH_SHORT, 163 | gravity: ToastGravity.BOTTOM, 164 | backgroundColor: Colors.white, 165 | textColor: Colors.black, 166 | ); 167 | } 168 | } 169 | 170 | Future _launchCoffee() async { 171 | final Uri coffeeUrl = Uri( 172 | scheme: 'https', 173 | path: 'ko-fi.com/nielslee', 174 | ); 175 | 176 | Fluttertoast.showToast( 177 | msg: '😊Have a nice day!', 178 | toastLength: Toast.LENGTH_SHORT, 179 | gravity: ToastGravity.BOTTOM, 180 | backgroundColor: Colors.white, 181 | textColor: Colors.black, 182 | ); 183 | 184 | // await Future.delayed(Duration(milliseconds: 100)); 185 | 186 | Fluttertoast.showToast( 187 | msg: '😊Have a nice day!', 188 | toastLength: Toast.LENGTH_LONG, 189 | gravity: ToastGravity.CENTER, 190 | backgroundColor: Colors.white, 191 | textColor: Colors.black, 192 | ); 193 | 194 | await launchUrl(coffeeUrl); 195 | } 196 | 197 | Widget _buildColorButton(Color color) { 198 | return Consumer( 199 | builder: (context, viewModel, child) { 200 | final isSelected = viewModel.seedColor.value == color.value; 201 | return GestureDetector( 202 | onTap: () { 203 | viewModel.updateSeedColor(color); 204 | Vibration.vibrate(amplitude: 255, duration: 5); 205 | }, 206 | child: Container( 207 | width: 48, 208 | height: 36, 209 | decoration: BoxDecoration( 210 | color: color, 211 | shape: BoxShape.circle, 212 | border: Border.all( 213 | color: isSelected ? Colors.white : Colors.transparent, 214 | width: 4, 215 | ), 216 | boxShadow: [ 217 | BoxShadow( 218 | color: Colors.black.withOpacity(0.2), 219 | blurRadius: 4, 220 | offset: Offset(0, 2), 221 | ), 222 | ], 223 | ), 224 | ), 225 | ); 226 | }, 227 | ); 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /lib/dialog/insert_saver_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:fluttertoast/fluttertoast.dart'; 5 | import 'package:path/path.dart'; 6 | import 'package:provider/provider.dart'; 7 | import 'package:snap_saver/dialog/more_dialog.dart'; 8 | import 'package:snap_saver/dialog/path_selector_entity.dart'; 9 | import 'package:snap_saver/entity/more.dart'; 10 | import 'package:snap_saver/viewmodel/dialog_view_model.dart'; 11 | import 'package:flutter_gen/gen_l10n/app_localizations.dart'; 12 | import 'package:vibration/vibration.dart'; 13 | 14 | import '../file/android_native_path_picker.dart'; 15 | 16 | class InsertSaverDialog extends StatefulWidget { 17 | const InsertSaverDialog({super.key}); 18 | 19 | @override 20 | State createState() => InsertSaverDialogState(); 21 | } 22 | 23 | class InsertSaverDialogState extends State { 24 | // List for path selectors 25 | List pathSelectors = [PathSelectorEntity()]; 26 | // Controller for input saver name and paths 27 | TextEditingController nameController = TextEditingController(); 28 | TextEditingController pathController = TextEditingController(); 29 | TextEditingController fileNameController = TextEditingController(); 30 | 31 | // Indicates whether user has manually input path 32 | bool hasManuallyInputPath = false; 33 | // Color of the Saver button 34 | Color? saverColor = Colors.transparent; 35 | // Selected color index in color list 36 | int selectedColorIndex = -1; 37 | 38 | @override 39 | Widget build(BuildContext context) { 40 | final colorList = [ 41 | Colors.red, 42 | Colors.orange, 43 | Colors.yellow, 44 | Colors.green, 45 | Colors.cyan, 46 | Colors.blue, 47 | Colors.purple 48 | ]; 49 | 50 | Future _showMoreDialog() async { 51 | return showGeneralDialog( 52 | context: context, 53 | barrierDismissible: true, 54 | barrierLabel: "More Dialog", 55 | pageBuilder: (BuildContext context, anim1, anmi2) { 56 | return MoreDialog(); 57 | }, 58 | ); 59 | } 60 | 61 | return ChangeNotifierProvider( 62 | create: (_) => DialogViewModel(), 63 | child: Consumer( 64 | builder: (_, dialogViewModel, __) { 65 | return AlertDialog( 66 | insetPadding: EdgeInsets.all(16), 67 | contentPadding: EdgeInsets.fromLTRB(16, 20, 16, 0), 68 | title: Text(AppLocalizations.of(context)!.createANewSaver), 69 | content: SingleChildScrollView( 70 | child: ListBody( 71 | children: [ 72 | // Text field for input name of new saver 73 | Text(AppLocalizations.of(context)!.saverName + ": "), 74 | TextField( 75 | controller: nameController, 76 | onTap: () { 77 | hasManuallyInputPath = true; 78 | }, 79 | decoration: InputDecoration( 80 | label: Text(AppLocalizations.of(context)! 81 | .saverNameDescription), 82 | floatingLabelBehavior: FloatingLabelBehavior.never, 83 | border: const OutlineInputBorder( 84 | borderRadius: 85 | BorderRadius.all(Radius.circular(12)))), 86 | ), 87 | 88 | Padding(padding: EdgeInsets.all(4)), 89 | 90 | // Columns for path select 91 | Column( 92 | children: pathSelectors.asMap().entries.map((entry) { 93 | final index = entry.key; 94 | final pathSelector = entry.value; 95 | 96 | return Row( 97 | mainAxisAlignment: MainAxisAlignment.end, 98 | children: [ 99 | //when path is not selected, show the select button 100 | Visibility( 101 | visible: !pathSelector.isPathSelected, 102 | child: Expanded( 103 | child: TextButton( 104 | onPressed: () { 105 | AndroidNativePathPicker() 106 | .selectPath((path) { 107 | if (path != null) { 108 | pathController.text = path; 109 | 110 | if (!hasManuallyInputPath) { 111 | // if user doesn't input name manually, use path's basename as default 112 | nameController.text += basename(path); 113 | nameController.text += " "; 114 | } 115 | 116 | setState(() { 117 | pathSelector.path = path; 118 | pathSelector.isPathSelected = true; 119 | }); 120 | // add selected path to viewmodel 121 | dialogViewModel.addPath(path); 122 | } 123 | }); 124 | }, 125 | child: Text( 126 | AppLocalizations.of(context)!.selectPath), 127 | style: OutlinedButton.styleFrom( 128 | backgroundColor: Colors.white, 129 | shape: RoundedRectangleBorder( 130 | borderRadius: 131 | BorderRadius.all(Radius.circular(12)), 132 | ), 133 | ), 134 | ), 135 | )), 136 | 137 | // if path is selected, show the path 138 | Visibility( 139 | visible: pathSelector.isPathSelected, 140 | child: Expanded( 141 | child: SingleChildScrollView( 142 | scrollDirection: Axis.horizontal, 143 | child: Text(pathSelector.path.toString())), 144 | )), 145 | 146 | // if this is the last column, show add button 147 | Visibility( 148 | visible: index == pathSelectors.length - 1, 149 | child: IconButton( 150 | onPressed: () { 151 | setState(() { 152 | pathSelectors.add(PathSelectorEntity()); 153 | print(pathSelectors.map((entity) { 154 | entity.path.toString(); 155 | }).toList()); 156 | }); 157 | }, 158 | icon: Icon(Icons.add))), 159 | 160 | // if path is selected, show edit button 161 | Visibility( 162 | visible: pathSelector.isPathSelected, 163 | child: IconButton( 164 | onPressed: () { 165 | setState(() {}); 166 | }, 167 | icon: Icon(Icons.edit))), 168 | 169 | // if more than one column(path selector) exist, show remove button 170 | Visibility( 171 | visible: pathSelectors.length != 1, 172 | child: IconButton( 173 | onPressed: () { 174 | setState(() { 175 | pathSelectors.removeAt(index); 176 | }); 177 | }, 178 | icon: Icon(Icons.remove))) 179 | ], 180 | ); 181 | }).toList()), 182 | 183 | // Row for select saver color 184 | SingleChildScrollView( 185 | scrollDirection: Axis.horizontal, 186 | child: Row( 187 | children: colorList.asMap().entries.map((colorEntry) { 188 | return IconButton( 189 | isSelected: selectedColorIndex == colorEntry.key, 190 | selectedIcon: Icon( 191 | Icons.check, 192 | color: colorEntry.value, 193 | ), 194 | onPressed: () { 195 | setState(() { 196 | selectedColorIndex = colorEntry.key; 197 | saverColor = colorList[selectedColorIndex]; 198 | }); 199 | }, 200 | icon: Icon(Icons.folder, color: colorEntry.value), 201 | ); 202 | }).toList()), 203 | ) 204 | ], 205 | ), 206 | ), 207 | actions: [ 208 | Row( 209 | children: [ 210 | // More Button 211 | TextButton( 212 | child: Text(AppLocalizations.of(context)!.more), 213 | onPressed: () async { 214 | Vibration.vibrate(amplitude: 255, duration: 5); 215 | var more = await _showMoreDialog(); 216 | if (more != null) { 217 | dialogViewModel.setPhotoName(more.photoName); 218 | dialogViewModel.setSuffixType(more.suffixType); 219 | Fluttertoast.showToast( 220 | msg: AppLocalizations.of(context)! 221 | .moreDialogFinished, 222 | toastLength: Toast.LENGTH_SHORT, 223 | gravity: ToastGravity.BOTTOM, 224 | backgroundColor: Colors.white, 225 | textColor: Colors.black, 226 | ); 227 | } 228 | }, 229 | ), 230 | 231 | Spacer(), 232 | 233 | // cancel button 234 | TextButton( 235 | child: Text(AppLocalizations.of(context)!.cancel), 236 | onPressed: () { 237 | Vibration.vibrate(amplitude: 255, duration: 5); 238 | Navigator.of(context).pop(); 239 | }, 240 | ), 241 | 242 | // ok button 243 | TextButton( 244 | child: Text(AppLocalizations.of(context)!.ok), 245 | style: TextButton.styleFrom(backgroundColor: saverColor), 246 | onPressed: () { 247 | bool hasSelectedPath = false; 248 | Vibration.vibrate(amplitude: 255, duration: 5); 249 | String inputName = nameController.text; 250 | 251 | List avaliablePaths = pathSelectors.map( 252 | (selector) { 253 | if (selector.isPathSelected) { 254 | hasSelectedPath = true; 255 | return selector.path; 256 | } 257 | }, 258 | ).toList(); 259 | 260 | if (inputName.isEmpty || !hasSelectedPath) { 261 | // no name or no selected path, do nothing 262 | } else { 263 | dialogViewModel.setName(inputName); 264 | if (selectedColorIndex >= 0) { 265 | dialogViewModel 266 | .setColor(colorList[selectedColorIndex]); 267 | } 268 | Navigator.of(context).pop(dialogViewModel); 269 | } 270 | }, 271 | ), 272 | ], 273 | ), 274 | ], 275 | ); 276 | }, 277 | )); 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /lib/home_screen.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:developer'; 3 | import 'dart:io'; 4 | 5 | import 'package:audioplayers/audioplayers.dart'; 6 | import 'package:camera/camera.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; 9 | import 'package:intl/intl.dart'; 10 | import 'package:path/path.dart'; 11 | import 'package:permission_handler/permission_handler.dart'; 12 | import 'package:provider/provider.dart'; 13 | import 'package:snap_saver/dialog/remove_saver_dialog.dart'; 14 | import 'package:snap_saver/viewmodel/home_view_model.dart'; 15 | import 'package:vibration/vibration.dart'; 16 | import 'package:path/path.dart' as path; 17 | import 'package:image/image.dart' as img; 18 | 19 | import 'entity/saver.dart'; 20 | 21 | class HomeScreen extends StatefulWidget { 22 | const HomeScreen({super.key}); 23 | 24 | @override 25 | HomeScreenState createState() => HomeScreenState(); 26 | } 27 | 28 | class HomeScreenState extends State { 29 | late List cameras; 30 | late int selectedLensIndex = 0; 31 | late CameraController _controller; 32 | double _minZoomLevel = 1 / 2; 33 | double _maxZoomLevel = 3; 34 | Future? _initializeControllerFuture; 35 | bool isCapturing = false; 36 | Offset? focusOffset = null; 37 | double _sliderValue = 1; 38 | ResolutionPreset currentResolution = ResolutionPreset.max; // default max 39 | int currentResolutionIndex = 0; 40 | var currentLensDirection = CameraLensDirection.back; 41 | 42 | @override 43 | void initState() { 44 | super.initState(); 45 | 46 | _initCamera(currentResolution); 47 | } 48 | 49 | Future _initCamera(ResolutionPreset resolution) async { 50 | WidgetsFlutterBinding.ensureInitialized(); 51 | 52 | cameras = await availableCameras(); 53 | await Permission.camera.request(); 54 | _controller = CameraController(cameras[selectedLensIndex], resolution, 55 | enableAudio: false); 56 | 57 | _initializeControllerFuture = _controller.initialize(); 58 | 59 | if (mounted) { 60 | setState(() {}); 61 | } 62 | await _resetZoomLevel(); 63 | } 64 | 65 | @override 66 | void dispose() { 67 | _controller.dispose(); 68 | super.dispose(); 69 | } 70 | 71 | Future _requestStoragePermission() async { 72 | PermissionStatus status = await Permission.manageExternalStorage.request(); 73 | 74 | if (status.isGranted) { 75 | // permission granted 76 | } else if (status.isDenied) { 77 | // permission denied 78 | } else if (status.isPermanentlyDenied) { 79 | // need go to settings 80 | await openAppSettings(); 81 | } 82 | } 83 | 84 | @override 85 | Widget build(BuildContext context) { 86 | int newResolution = context.watch().resolution; 87 | if (newResolution != currentResolutionIndex) { 88 | switch (context.watch().resolution) { 89 | case 0: 90 | _initCamera(ResolutionPreset.low); 91 | case 1: 92 | _initCamera(ResolutionPreset.medium); 93 | case 2: 94 | _initCamera(ResolutionPreset.high); 95 | case 3: 96 | _initCamera(ResolutionPreset.veryHigh); 97 | case 4: 98 | _initCamera(ResolutionPreset.ultraHigh); 99 | case 5: 100 | _initCamera(ResolutionPreset.max); 101 | } 102 | currentResolutionIndex = newResolution; 103 | } 104 | 105 | return Consumer( 106 | builder: (BuildContext context, HomeViewModel viewModel, Widget? child) { 107 | final itemList = viewModel.savers; 108 | final saversRowPadding = MediaQuery.of(context).size.width * 0.1; 109 | 110 | return Column( 111 | children: [ 112 | AspectRatio( 113 | aspectRatio: 3 / 4, 114 | child: Container( 115 | padding: const EdgeInsets.all(4), 116 | child: FutureBuilder( 117 | future: _initializeControllerFuture, 118 | builder: (context, snapshot) { 119 | if (snapshot.connectionState == ConnectionState.done && 120 | !isCapturing) { 121 | return LayoutBuilder( 122 | builder: 123 | (BuildContext context, BoxConstraints constraints) { 124 | return Stack( 125 | alignment: AlignmentDirectional.bottomCenter, 126 | children: [ 127 | // 使用裁剪方式:保持宽度不变,高度对称裁剪 128 | ClipRRect( 129 | borderRadius: BorderRadius.circular(12), 130 | child: SizedBox( 131 | width: constraints.maxWidth, 132 | height: constraints.maxWidth * 4 / 3, 133 | child: ClipRect( 134 | child: Align( 135 | alignment: Alignment.center, 136 | child: Builder( 137 | builder: (context) { 138 | // 获取相机预览的实际尺寸 139 | final previewSize = 140 | _controller.value.previewSize; 141 | if (previewSize == null || 142 | previewSize.height == 0) { 143 | return Center( 144 | child: 145 | CircularProgressIndicator()); 146 | } 147 | 148 | // 计算相机预览的实际宽高比 149 | final previewAspectRatio = 150 | previewSize.height / 151 | previewSize.width; 152 | 153 | // 目标宽度 154 | final targetWidth = 155 | constraints.maxWidth; 156 | 157 | // 使用 AspectRatio 让 CameraPreview 按原始比例显示 158 | // 然后用 Transform.scale 等比例放大,使宽度等于目标宽度 159 | // 使用较小的基准宽度,让 AspectRatio 有约束可计算 160 | final baseWidth = 100.0; 161 | 162 | // 计算缩放比例,使放大后的宽度等于目标宽度 163 | final scale = targetWidth / baseWidth; 164 | 165 | return Center( 166 | child: Transform.scale( 167 | scale: scale, 168 | alignment: Alignment.center, 169 | child: SizedBox( 170 | width: baseWidth, 171 | child: AspectRatio( 172 | aspectRatio: 173 | previewAspectRatio, 174 | child: CameraPreview( 175 | _controller), 176 | ), 177 | ), 178 | ), 179 | ); 180 | }, 181 | ), 182 | ), 183 | ), 184 | ), 185 | ), 186 | // 添加触摸监听层 187 | Positioned.fill( 188 | child: Listener( 189 | onPointerDown: (downEvent) { 190 | _onCameraPreviewTap(downEvent, constraints); 191 | setState(() { 192 | focusOffset = Offset( 193 | downEvent.localPosition.dx, 194 | downEvent.localPosition.dy, 195 | ); 196 | }); 197 | // delay 1 second and remove focus box 198 | Timer(Duration(seconds: 1), () { 199 | setState(() { 200 | focusOffset = null; 201 | }); 202 | }); 203 | }, 204 | behavior: HitTestBehavior.translucent, 205 | child: Container( 206 | color: Colors.transparent, 207 | ), 208 | ), 209 | ), 210 | // 对焦框绘制层(放在最上层,确保显示) 211 | Positioned.fill( 212 | child: IgnorePointer( 213 | child: CustomPaint( 214 | painter: FocusBoxPainter(focusOffset), 215 | ), 216 | ), 217 | ), 218 | Container( 219 | height: 48, 220 | child: Row( 221 | mainAxisAlignment: MainAxisAlignment.center, 222 | children: [ 223 | Slider( 224 | value: _sliderValue, 225 | min: _zoom2Slider(_minZoomLevel), 226 | max: _zoom2Slider(_maxZoomLevel), 227 | onChanged: (newValue) { 228 | setState(() { 229 | _controller.setZoomLevel( 230 | _slider2Zoom(newValue)); 231 | _sliderValue = newValue; 232 | }); 233 | }, 234 | ), 235 | Spacer(), 236 | IconButton( 237 | onPressed: () { 238 | if (currentLensDirection == 239 | CameraLensDirection.back) { 240 | currentLensDirection = 241 | CameraLensDirection.front; 242 | } else { 243 | currentLensDirection = 244 | CameraLensDirection.back; 245 | } 246 | 247 | for (var (lensIndex, cameraLens) 248 | in cameras.indexed) { 249 | if (cameraLens.lensDirection == 250 | currentLensDirection) { 251 | setState(() { 252 | Vibration.vibrate( 253 | amplitude: 255, 254 | duration: 5); 255 | selectedLensIndex = lensIndex; 256 | _initCamera(currentResolution); 257 | }); 258 | } 259 | } 260 | }, 261 | icon: Icon(Icons.cameraswitch)), 262 | ], 263 | ), 264 | ) 265 | ], 266 | ); 267 | }, 268 | ); 269 | } else { 270 | // Otherwise, display a loading indicator. 271 | return const Center(child: CircularProgressIndicator()); 272 | } 273 | }, 274 | ), 275 | ), 276 | ), 277 | Expanded( 278 | child: MasonryGridView.builder( 279 | padding: EdgeInsets.fromLTRB( 280 | saversRowPadding, 0, saversRowPadding, 12), 281 | itemCount: itemList.length, 282 | scrollDirection: Axis.horizontal, 283 | gridDelegate: 284 | const SliverSimpleGridDelegateWithFixedCrossAxisCount( 285 | crossAxisCount: 2), 286 | itemBuilder: (context, index) { 287 | ColorScheme saverColorScheme = 288 | ColorScheme.fromSeed(seedColor: viewModel.seedColor); 289 | 290 | // generate Saver color scheme 291 | if (itemList[index].color != null) { 292 | saverColorScheme = ColorScheme.fromSeed( 293 | seedColor: Color(itemList[index].color!)); 294 | } 295 | 296 | // Function for taking photos 297 | Future _takePhotos() async { 298 | if (isCapturing) { 299 | // if is capturing, skip 300 | return; 301 | } 302 | try { 303 | await _initializeControllerFuture; 304 | 305 | setState(() { 306 | isCapturing = true; 307 | }); 308 | 309 | await Vibration.vibrate(amplitude: 255, duration: 5); 310 | await AudioPlayer() 311 | .play(AssetSource('sounds/camera_shutter.mp3')); 312 | final image = await _controller.takePicture(); 313 | debugPrint( 314 | "take photo result: path: ${image.path} name: ${image.name}"); 315 | 316 | setState(() { 317 | isCapturing = false; 318 | }); 319 | 320 | _requestStoragePermission(); 321 | 322 | // TODO add a progress animate in Saver button 323 | final saver = itemList[index]; 324 | final paths = saver.paths; 325 | 326 | final prefixedFileName = getPrefixedFileName(saver); 327 | await handleFileAspectRatio(image); 328 | await moveXFileToFile(image, paths, prefixedFileName); 329 | 330 | saver.count++; 331 | viewModel.updateSaver(saver); 332 | 333 | if (!context.mounted) return; 334 | } catch (e) { 335 | log(e.toString()); 336 | } 337 | } 338 | 339 | Future _showRemoveDialog() async { 340 | return showGeneralDialog( 341 | context: context, 342 | barrierDismissible: true, 343 | barrierLabel: "Remove Saver Dialog", 344 | pageBuilder: (BuildContext context, anim1, anmi2) { 345 | return RemoveSaverDialog(saver: itemList[index]); 346 | }, 347 | ); 348 | } 349 | 350 | // Saver button 351 | return Container( 352 | margin: const EdgeInsets.all(4), 353 | child: ElevatedButton( 354 | onLongPress: () async { 355 | final remove = await _showRemoveDialog(); 356 | if (remove == true) { 357 | viewModel.removeSaver(itemList[index]); 358 | } 359 | }, 360 | onPressed: _takePhotos, 361 | child: Badge( 362 | isLabelVisible: 363 | (itemList[index].suffixType % 2 == 0), 364 | backgroundColor: Colors.deepOrange, 365 | offset: Offset(16, -16), 366 | label: Text(itemList[index].count.toString()), 367 | child: Text(itemList[index].name)), 368 | style: ElevatedButton.styleFrom( 369 | backgroundColor: saverColorScheme.primaryContainer, 370 | foregroundColor: saverColorScheme.onPrimaryContainer, 371 | ), 372 | ), 373 | ); 374 | }), 375 | ), 376 | ], 377 | ); 378 | }, 379 | ); 380 | } 381 | 382 | double _zoom2Slider(double zoomValue) { 383 | if (zoomValue >= 1) { 384 | return zoomValue; 385 | } else { 386 | return 0 - (1 / zoomValue); 387 | } 388 | } 389 | 390 | double _slider2Zoom(double sliderValue) { 391 | if (sliderValue >= 0) { 392 | return sliderValue; 393 | } else { 394 | return (1 / sliderValue) + 1; 395 | } 396 | } 397 | 398 | Future _resetZoomLevel() async { 399 | await _controller.initialize(); 400 | double min = await _controller.getMinZoomLevel(); 401 | double max = await _controller.getMaxZoomLevel(); 402 | 403 | setState(() { 404 | _minZoomLevel = min; 405 | _maxZoomLevel = max; 406 | _sliderValue = 1; 407 | }); 408 | } 409 | 410 | void _onCameraPreviewTap(PointerDownEvent event, BoxConstraints constraints) { 411 | final x = event.localPosition.dx / constraints.maxWidth; 412 | final y = event.localPosition.dy / constraints.maxHeight; 413 | debugPrint("onCameraPreviewTap: x: ${x}, y: ${y}"); 414 | 415 | _controller.setFocusPoint(Offset(x, y)); 416 | } 417 | } 418 | 419 | String? getPrefixedFileName(Saver saver) { 420 | var newName = saver.photoName; 421 | if (newName != null) { 422 | switch (saver.suffixType) { 423 | case 0: 424 | { 425 | newName = newName + saver.count.toString(); 426 | } 427 | case 1: 428 | { 429 | DateTime now = DateTime.now(); 430 | String nowStr = DateFormat('yyyyMMddHHmmss').format(now); 431 | newName += nowStr; 432 | } 433 | case 2: 434 | { 435 | newName = newName + '_' + saver.count.toString(); 436 | } 437 | case 3: 438 | { 439 | DateTime now = DateTime.now(); 440 | String nowStr = DateFormat('yyyyMMddHHmmss').format(now); 441 | newName += '_' + nowStr; 442 | } 443 | case 4: 444 | { 445 | newName = newName + '-' + saver.count.toString(); 446 | } 447 | case 5: 448 | { 449 | DateTime now = DateTime.now(); 450 | String nowStr = DateFormat('yyyyMMddHHmmss').format(now); 451 | newName += '-' + nowStr; 452 | } 453 | } 454 | } 455 | return newName; 456 | } 457 | 458 | /** 459 | * 裁剪图片到3:4比例 460 | */ 461 | Future handleFileAspectRatio(XFile imageFile) async { 462 | try { 463 | // 读取图片字节 464 | final bytes = await imageFile.readAsBytes(); 465 | final image = img.decodeImage(bytes); 466 | if (image == null) { 467 | debugPrint('无法解析图片内容'); 468 | return false; 469 | } 470 | final int width = image.width; 471 | final int height = image.height; 472 | 473 | // 若宽高比已为3:4, 不处理 474 | if ((width * 4).toDouble() == (height * 3).toDouble() || 475 | (width / height - 0.75).abs() < 0.01) { 476 | // 3:4 比例 477 | return true; 478 | } 479 | 480 | // 计算目标高度 481 | int targetHeight = (width * 4 / 3).round(); 482 | 483 | // 如果实际高度小于目标高度, 则不能裁剪 484 | if (height < targetHeight) { 485 | debugPrint('图片高度不足以裁剪为3:4, 跳过'); 486 | return false; 487 | } 488 | 489 | // 取中间部分 490 | int offsetY = ((height - targetHeight) / 2).round(); 491 | 492 | // 裁剪图片 493 | final cropped = img.copyCrop( 494 | image, 495 | x: 0, 496 | y: offsetY, 497 | width: width, 498 | height: targetHeight, 499 | ); 500 | 501 | // 重新保存图片(覆盖原文件) 502 | final extension = path.extension(imageFile.path).toLowerCase(); 503 | List encodedBytes; 504 | if (extension == ".png") { 505 | encodedBytes = img.encodePng(cropped); 506 | } else { 507 | // 默认jpg 508 | encodedBytes = img.encodeJpg(cropped); 509 | } 510 | final file = File(imageFile.path); 511 | await file.writeAsBytes(encodedBytes, flush: true); 512 | 513 | debugPrint('handleFileAspectRatio success: ${imageFile.path}'); 514 | return true; 515 | } catch (e) { 516 | debugPrint('handleFileAspectRatio error: $e'); 517 | return false; 518 | } 519 | } 520 | 521 | Future moveXFileToFile( 522 | XFile xFile, List destinationPaths, String? newName) async { 523 | File sourceFile = File(xFile.path); 524 | bool isSucceed = true; 525 | 526 | try { 527 | for (String destinationPath in destinationPaths) { 528 | final sourceFileName = basename(sourceFile.path); 529 | String extension = path.extension(sourceFileName); 530 | File destinationFile = File('$destinationPath/$sourceFileName'); 531 | 532 | if (newName != null) { 533 | await sourceFile 534 | .copy(destinationFile.parent.path + '/$newName$extension'); 535 | } else { 536 | await sourceFile.copy(destinationFile.path); 537 | } 538 | 539 | log('File moved to: ${destinationFile.parent.path + '/$newName$extension'}'); 540 | } 541 | } catch (e) { 542 | isSucceed = false; 543 | debugPrint('Error moving file: $e'); 544 | } finally { 545 | await sourceFile.delete(); 546 | } 547 | return isSucceed; 548 | } 549 | 550 | class FocusBoxPainter extends CustomPainter { 551 | final Offset? boxOffset; 552 | final double boxSize = 50; 553 | 554 | FocusBoxPainter(this.boxOffset); 555 | 556 | @override 557 | void paint(Canvas canvas, Size size) { 558 | if (boxOffset == null) { 559 | return; 560 | } 561 | final Paint paint = Paint() 562 | ..color = Colors.white 563 | ..style = PaintingStyle.stroke 564 | ..strokeWidth = 2; 565 | 566 | final Rect rect = Rect.fromLTWH( 567 | boxOffset!.dx - boxSize / 2, 568 | boxOffset!.dy - boxSize / 2, 569 | boxSize, 570 | boxSize, 571 | ); 572 | canvas.drawRect(rect, paint); 573 | } 574 | 575 | @override 576 | bool shouldRepaint(covariant CustomPainter oldDelegate) { 577 | return true; // 每次都需要重绘 578 | } 579 | } 580 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | archive: 5 | dependency: transitive 6 | description: 7 | name: archive 8 | sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "4.0.7" 12 | args: 13 | dependency: transitive 14 | description: 15 | name: args 16 | sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "2.7.0" 20 | async: 21 | dependency: transitive 22 | description: 23 | name: async 24 | sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "2.11.0" 28 | audioplayers: 29 | dependency: "direct main" 30 | description: 31 | name: audioplayers 32 | sha256: a5341380a4f1d3a10a4edde5bb75de5127fe31e0faa8c4d860e64d2f91ad84c7 33 | url: "https://pub.dev" 34 | source: hosted 35 | version: "6.4.0" 36 | audioplayers_android: 37 | dependency: transitive 38 | description: 39 | name: audioplayers_android 40 | sha256: f8c90823a45b475d2c129f85bbda9c029c8d4450b172f62e066564c6e170f69a 41 | url: "https://pub.dev" 42 | source: hosted 43 | version: "5.2.0" 44 | audioplayers_darwin: 45 | dependency: transitive 46 | description: 47 | name: audioplayers_darwin 48 | sha256: "405cdbd53ebdb4623f1c5af69f275dad4f930ce895512d5261c07cd95d23e778" 49 | url: "https://pub.dev" 50 | source: hosted 51 | version: "6.2.0" 52 | audioplayers_linux: 53 | dependency: transitive 54 | description: 55 | name: audioplayers_linux 56 | sha256: "7e0d081a6a527c53aef9539691258a08ff69a7dc15ef6335fbea1b4b03ebbef0" 57 | url: "https://pub.dev" 58 | source: hosted 59 | version: "4.2.0" 60 | audioplayers_platform_interface: 61 | dependency: transitive 62 | description: 63 | name: audioplayers_platform_interface 64 | sha256: "77e5fa20fb4a64709158391c75c1cca69a481d35dc879b519e350a05ff520373" 65 | url: "https://pub.dev" 66 | source: hosted 67 | version: "7.1.0" 68 | audioplayers_web: 69 | dependency: transitive 70 | description: 71 | name: audioplayers_web 72 | sha256: bd99d8821114747682a2be0adcdb70233d4697af989b549d3a20a0f49f6c9b13 73 | url: "https://pub.dev" 74 | source: hosted 75 | version: "5.1.0" 76 | audioplayers_windows: 77 | dependency: transitive 78 | description: 79 | name: audioplayers_windows 80 | sha256: "871d3831c25cd2408ddc552600fd4b32fba675943e319a41284704ee038ad563" 81 | url: "https://pub.dev" 82 | source: hosted 83 | version: "4.2.0" 84 | boolean_selector: 85 | dependency: transitive 86 | description: 87 | name: boolean_selector 88 | sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" 89 | url: "https://pub.dev" 90 | source: hosted 91 | version: "2.1.1" 92 | camera: 93 | dependency: "direct main" 94 | description: 95 | name: camera 96 | sha256: "26ff41045772153f222ffffecba711a206f670f5834d40ebf5eed3811692f167" 97 | url: "https://pub.dev" 98 | source: hosted 99 | version: "0.11.0+2" 100 | camera_android_camerax: 101 | dependency: transitive 102 | description: 103 | name: camera_android_camerax 104 | sha256: "59967e6d80df9d682a33b86f228cc524e6b52d6184b84f6ac62151dd98bd1ea0" 105 | url: "https://pub.dev" 106 | source: hosted 107 | version: "0.6.5+2" 108 | camera_avfoundation: 109 | dependency: transitive 110 | description: 111 | name: camera_avfoundation 112 | sha256: "2e4c568f70e406ccb87376bc06b53d2f5bebaab71e2fbcc1a950e31449381bcf" 113 | url: "https://pub.dev" 114 | source: hosted 115 | version: "0.9.17+5" 116 | camera_platform_interface: 117 | dependency: transitive 118 | description: 119 | name: camera_platform_interface 120 | sha256: b3ede1f171532e0d83111fe0980b46d17f1aa9788a07a2fbed07366bbdbb9061 121 | url: "https://pub.dev" 122 | source: hosted 123 | version: "2.8.0" 124 | camera_web: 125 | dependency: transitive 126 | description: 127 | name: camera_web 128 | sha256: "595f28c89d1fb62d77c73c633193755b781c6d2e0ebcd8dc25b763b514e6ba8f" 129 | url: "https://pub.dev" 130 | source: hosted 131 | version: "0.3.5" 132 | characters: 133 | dependency: transitive 134 | description: 135 | name: characters 136 | sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" 137 | url: "https://pub.dev" 138 | source: hosted 139 | version: "1.3.0" 140 | checked_yaml: 141 | dependency: transitive 142 | description: 143 | name: checked_yaml 144 | sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff 145 | url: "https://pub.dev" 146 | source: hosted 147 | version: "2.0.3" 148 | cli_util: 149 | dependency: transitive 150 | description: 151 | name: cli_util 152 | sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 153 | url: "https://pub.dev" 154 | source: hosted 155 | version: "0.4.1" 156 | clock: 157 | dependency: transitive 158 | description: 159 | name: clock 160 | sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf 161 | url: "https://pub.dev" 162 | source: hosted 163 | version: "1.1.1" 164 | collection: 165 | dependency: transitive 166 | description: 167 | name: collection 168 | sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a 169 | url: "https://pub.dev" 170 | source: hosted 171 | version: "1.18.0" 172 | cross_file: 173 | dependency: transitive 174 | description: 175 | name: cross_file 176 | sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" 177 | url: "https://pub.dev" 178 | source: hosted 179 | version: "0.3.4+2" 180 | crypto: 181 | dependency: transitive 182 | description: 183 | name: crypto 184 | sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab 185 | url: "https://pub.dev" 186 | source: hosted 187 | version: "3.0.3" 188 | cupertino_icons: 189 | dependency: "direct main" 190 | description: 191 | name: cupertino_icons 192 | sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 193 | url: "https://pub.dev" 194 | source: hosted 195 | version: "1.0.8" 196 | device_info_plus: 197 | dependency: transitive 198 | description: 199 | name: device_info_plus 200 | sha256: a7fd703482b391a87d60b6061d04dfdeab07826b96f9abd8f5ed98068acc0074 201 | url: "https://pub.dev" 202 | source: hosted 203 | version: "10.1.2" 204 | device_info_plus_platform_interface: 205 | dependency: transitive 206 | description: 207 | name: device_info_plus_platform_interface 208 | sha256: "0b04e02b30791224b31969eb1b50d723498f402971bff3630bca2ba839bd1ed2" 209 | url: "https://pub.dev" 210 | source: hosted 211 | version: "7.0.2" 212 | dynamic_color: 213 | dependency: "direct main" 214 | description: 215 | name: dynamic_color 216 | sha256: "43a5a6679649a7731ab860334a5812f2067c2d9ce6452cf069c5e0c25336c17c" 217 | url: "https://pub.dev" 218 | source: hosted 219 | version: "1.8.1" 220 | fake_async: 221 | dependency: transitive 222 | description: 223 | name: fake_async 224 | sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" 225 | url: "https://pub.dev" 226 | source: hosted 227 | version: "1.3.1" 228 | ffi: 229 | dependency: transitive 230 | description: 231 | name: ffi 232 | sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" 233 | url: "https://pub.dev" 234 | source: hosted 235 | version: "2.1.3" 236 | file: 237 | dependency: transitive 238 | description: 239 | name: file 240 | sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 241 | url: "https://pub.dev" 242 | source: hosted 243 | version: "7.0.1" 244 | file_picker: 245 | dependency: "direct main" 246 | description: 247 | name: file_picker 248 | sha256: "2ca051989f69d1b2ca012b2cf3ccf78c70d40144f0861ff2c063493f7c8c3d45" 249 | url: "https://pub.dev" 250 | source: hosted 251 | version: "8.0.5" 252 | fixnum: 253 | dependency: transitive 254 | description: 255 | name: fixnum 256 | sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be 257 | url: "https://pub.dev" 258 | source: hosted 259 | version: "1.1.1" 260 | flutter: 261 | dependency: "direct main" 262 | description: flutter 263 | source: sdk 264 | version: "0.0.0" 265 | flutter_launcher_icons: 266 | dependency: "direct main" 267 | description: 268 | name: flutter_launcher_icons 269 | sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea" 270 | url: "https://pub.dev" 271 | source: hosted 272 | version: "0.13.1" 273 | flutter_localization: 274 | dependency: "direct main" 275 | description: 276 | name: flutter_localization 277 | sha256: c2918388c56fe2d555074215dba44bc47bd5c1036e6b237f72f210b7136cfe61 278 | url: "https://pub.dev" 279 | source: hosted 280 | version: "0.2.1" 281 | flutter_localizations: 282 | dependency: "direct dev" 283 | description: flutter 284 | source: sdk 285 | version: "0.0.0" 286 | flutter_plugin_android_lifecycle: 287 | dependency: transitive 288 | description: 289 | name: flutter_plugin_android_lifecycle 290 | sha256: "8cf40eebf5dec866a6d1956ad7b4f7016e6c0cc69847ab946833b7d43743809f" 291 | url: "https://pub.dev" 292 | source: hosted 293 | version: "2.0.19" 294 | flutter_staggered_grid_view: 295 | dependency: "direct main" 296 | description: 297 | name: flutter_staggered_grid_view 298 | sha256: "19e7abb550c96fbfeb546b23f3ff356ee7c59a019a651f8f102a4ba9b7349395" 299 | url: "https://pub.dev" 300 | source: hosted 301 | version: "0.7.0" 302 | flutter_test: 303 | dependency: "direct dev" 304 | description: flutter 305 | source: sdk 306 | version: "0.0.0" 307 | flutter_web_plugins: 308 | dependency: transitive 309 | description: flutter 310 | source: sdk 311 | version: "0.0.0" 312 | fluttertoast: 313 | dependency: "direct main" 314 | description: 315 | name: fluttertoast 316 | sha256: "95f349437aeebe524ef7d6c9bde3e6b4772717cf46a0eb6a3ceaddc740b297cc" 317 | url: "https://pub.dev" 318 | source: hosted 319 | version: "8.2.8" 320 | http: 321 | dependency: transitive 322 | description: 323 | name: http 324 | sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 325 | url: "https://pub.dev" 326 | source: hosted 327 | version: "1.2.2" 328 | http_parser: 329 | dependency: transitive 330 | description: 331 | name: http_parser 332 | sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" 333 | url: "https://pub.dev" 334 | source: hosted 335 | version: "4.0.2" 336 | image: 337 | dependency: "direct main" 338 | description: 339 | name: image 340 | sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" 341 | url: "https://pub.dev" 342 | source: hosted 343 | version: "4.5.4" 344 | intl: 345 | dependency: "direct main" 346 | description: 347 | name: intl 348 | sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" 349 | url: "https://pub.dev" 350 | source: hosted 351 | version: "0.18.1" 352 | json_annotation: 353 | dependency: transitive 354 | description: 355 | name: json_annotation 356 | sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" 357 | url: "https://pub.dev" 358 | source: hosted 359 | version: "4.9.0" 360 | leak_tracker: 361 | dependency: transitive 362 | description: 363 | name: leak_tracker 364 | sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" 365 | url: "https://pub.dev" 366 | source: hosted 367 | version: "10.0.0" 368 | leak_tracker_flutter_testing: 369 | dependency: transitive 370 | description: 371 | name: leak_tracker_flutter_testing 372 | sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 373 | url: "https://pub.dev" 374 | source: hosted 375 | version: "2.0.1" 376 | leak_tracker_testing: 377 | dependency: transitive 378 | description: 379 | name: leak_tracker_testing 380 | sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 381 | url: "https://pub.dev" 382 | source: hosted 383 | version: "2.0.1" 384 | matcher: 385 | dependency: transitive 386 | description: 387 | name: matcher 388 | sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb 389 | url: "https://pub.dev" 390 | source: hosted 391 | version: "0.12.16+1" 392 | material_color_utilities: 393 | dependency: transitive 394 | description: 395 | name: material_color_utilities 396 | sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" 397 | url: "https://pub.dev" 398 | source: hosted 399 | version: "0.8.0" 400 | meta: 401 | dependency: transitive 402 | description: 403 | name: meta 404 | sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 405 | url: "https://pub.dev" 406 | source: hosted 407 | version: "1.11.0" 408 | nested: 409 | dependency: transitive 410 | description: 411 | name: nested 412 | sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" 413 | url: "https://pub.dev" 414 | source: hosted 415 | version: "1.0.0" 416 | path: 417 | dependency: "direct main" 418 | description: 419 | name: path 420 | sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" 421 | url: "https://pub.dev" 422 | source: hosted 423 | version: "1.9.0" 424 | path_provider: 425 | dependency: "direct main" 426 | description: 427 | name: path_provider 428 | sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 429 | url: "https://pub.dev" 430 | source: hosted 431 | version: "2.1.4" 432 | path_provider_android: 433 | dependency: transitive 434 | description: 435 | name: path_provider_android 436 | sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d 437 | url: "https://pub.dev" 438 | source: hosted 439 | version: "2.2.4" 440 | path_provider_foundation: 441 | dependency: transitive 442 | description: 443 | name: path_provider_foundation 444 | sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" 445 | url: "https://pub.dev" 446 | source: hosted 447 | version: "2.4.1" 448 | path_provider_linux: 449 | dependency: transitive 450 | description: 451 | name: path_provider_linux 452 | sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 453 | url: "https://pub.dev" 454 | source: hosted 455 | version: "2.2.1" 456 | path_provider_platform_interface: 457 | dependency: transitive 458 | description: 459 | name: path_provider_platform_interface 460 | sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" 461 | url: "https://pub.dev" 462 | source: hosted 463 | version: "2.1.2" 464 | path_provider_windows: 465 | dependency: transitive 466 | description: 467 | name: path_provider_windows 468 | sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 469 | url: "https://pub.dev" 470 | source: hosted 471 | version: "2.3.0" 472 | permission_handler: 473 | dependency: "direct main" 474 | description: 475 | name: permission_handler 476 | sha256: "18bf33f7fefbd812f37e72091a15575e72d5318854877e0e4035a24ac1113ecb" 477 | url: "https://pub.dev" 478 | source: hosted 479 | version: "11.3.1" 480 | permission_handler_android: 481 | dependency: transitive 482 | description: 483 | name: permission_handler_android 484 | sha256: "71bbecfee799e65aff7c744761a57e817e73b738fedf62ab7afd5593da21f9f1" 485 | url: "https://pub.dev" 486 | source: hosted 487 | version: "12.0.13" 488 | permission_handler_apple: 489 | dependency: transitive 490 | description: 491 | name: permission_handler_apple 492 | sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023 493 | url: "https://pub.dev" 494 | source: hosted 495 | version: "9.4.7" 496 | permission_handler_html: 497 | dependency: transitive 498 | description: 499 | name: permission_handler_html 500 | sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" 501 | url: "https://pub.dev" 502 | source: hosted 503 | version: "0.1.3+5" 504 | permission_handler_platform_interface: 505 | dependency: transitive 506 | description: 507 | name: permission_handler_platform_interface 508 | sha256: e9c8eadee926c4532d0305dff94b85bf961f16759c3af791486613152af4b4f9 509 | url: "https://pub.dev" 510 | source: hosted 511 | version: "4.2.3" 512 | permission_handler_windows: 513 | dependency: transitive 514 | description: 515 | name: permission_handler_windows 516 | sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" 517 | url: "https://pub.dev" 518 | source: hosted 519 | version: "0.2.1" 520 | petitparser: 521 | dependency: transitive 522 | description: 523 | name: petitparser 524 | sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 525 | url: "https://pub.dev" 526 | source: hosted 527 | version: "6.0.2" 528 | platform: 529 | dependency: transitive 530 | description: 531 | name: platform 532 | sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" 533 | url: "https://pub.dev" 534 | source: hosted 535 | version: "3.1.6" 536 | plugin_platform_interface: 537 | dependency: transitive 538 | description: 539 | name: plugin_platform_interface 540 | sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" 541 | url: "https://pub.dev" 542 | source: hosted 543 | version: "2.1.8" 544 | posix: 545 | dependency: transitive 546 | description: 547 | name: posix 548 | sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" 549 | url: "https://pub.dev" 550 | source: hosted 551 | version: "6.0.3" 552 | provider: 553 | dependency: "direct main" 554 | description: 555 | name: provider 556 | sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" 557 | url: "https://pub.dev" 558 | source: hosted 559 | version: "6.1.5+1" 560 | shared_preferences: 561 | dependency: "direct main" 562 | description: 563 | name: shared_preferences 564 | sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180 565 | url: "https://pub.dev" 566 | source: hosted 567 | version: "2.2.3" 568 | shared_preferences_android: 569 | dependency: transitive 570 | description: 571 | name: shared_preferences_android 572 | sha256: "1ee8bf911094a1b592de7ab29add6f826a7331fb854273d55918693d5364a1f2" 573 | url: "https://pub.dev" 574 | source: hosted 575 | version: "2.2.2" 576 | shared_preferences_foundation: 577 | dependency: transitive 578 | description: 579 | name: shared_preferences_foundation 580 | sha256: "07e050c7cd39bad516f8d64c455f04508d09df104be326d8c02551590a0d513d" 581 | url: "https://pub.dev" 582 | source: hosted 583 | version: "2.5.3" 584 | shared_preferences_linux: 585 | dependency: transitive 586 | description: 587 | name: shared_preferences_linux 588 | sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" 589 | url: "https://pub.dev" 590 | source: hosted 591 | version: "2.4.1" 592 | shared_preferences_platform_interface: 593 | dependency: transitive 594 | description: 595 | name: shared_preferences_platform_interface 596 | sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" 597 | url: "https://pub.dev" 598 | source: hosted 599 | version: "2.4.1" 600 | shared_preferences_web: 601 | dependency: transitive 602 | description: 603 | name: shared_preferences_web 604 | sha256: "59dc807b94d29d52ddbb1b3c0d3b9d0a67fc535a64e62a5542c8db0513fcb6c2" 605 | url: "https://pub.dev" 606 | source: hosted 607 | version: "2.4.1" 608 | shared_preferences_windows: 609 | dependency: transitive 610 | description: 611 | name: shared_preferences_windows 612 | sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" 613 | url: "https://pub.dev" 614 | source: hosted 615 | version: "2.4.1" 616 | sky_engine: 617 | dependency: transitive 618 | description: flutter 619 | source: sdk 620 | version: "0.0.99" 621 | source_span: 622 | dependency: transitive 623 | description: 624 | name: source_span 625 | sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" 626 | url: "https://pub.dev" 627 | source: hosted 628 | version: "1.10.0" 629 | sprintf: 630 | dependency: transitive 631 | description: 632 | name: sprintf 633 | sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" 634 | url: "https://pub.dev" 635 | source: hosted 636 | version: "7.0.0" 637 | sqflite: 638 | dependency: "direct main" 639 | description: 640 | name: sqflite 641 | sha256: a43e5a27235518c03ca238e7b4732cf35eabe863a369ceba6cbefa537a66f16d 642 | url: "https://pub.dev" 643 | source: hosted 644 | version: "2.3.3+1" 645 | sqflite_common: 646 | dependency: transitive 647 | description: 648 | name: sqflite_common 649 | sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4" 650 | url: "https://pub.dev" 651 | source: hosted 652 | version: "2.5.4" 653 | stack_trace: 654 | dependency: transitive 655 | description: 656 | name: stack_trace 657 | sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" 658 | url: "https://pub.dev" 659 | source: hosted 660 | version: "1.11.1" 661 | stream_channel: 662 | dependency: transitive 663 | description: 664 | name: stream_channel 665 | sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 666 | url: "https://pub.dev" 667 | source: hosted 668 | version: "2.1.2" 669 | stream_transform: 670 | dependency: transitive 671 | description: 672 | name: stream_transform 673 | sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 674 | url: "https://pub.dev" 675 | source: hosted 676 | version: "2.1.1" 677 | string_scanner: 678 | dependency: transitive 679 | description: 680 | name: string_scanner 681 | sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" 682 | url: "https://pub.dev" 683 | source: hosted 684 | version: "1.2.0" 685 | synchronized: 686 | dependency: transitive 687 | description: 688 | name: synchronized 689 | sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" 690 | url: "https://pub.dev" 691 | source: hosted 692 | version: "3.1.0+1" 693 | term_glyph: 694 | dependency: transitive 695 | description: 696 | name: term_glyph 697 | sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 698 | url: "https://pub.dev" 699 | source: hosted 700 | version: "1.2.1" 701 | test_api: 702 | dependency: transitive 703 | description: 704 | name: test_api 705 | sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" 706 | url: "https://pub.dev" 707 | source: hosted 708 | version: "0.6.1" 709 | typed_data: 710 | dependency: transitive 711 | description: 712 | name: typed_data 713 | sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c 714 | url: "https://pub.dev" 715 | source: hosted 716 | version: "1.3.2" 717 | url_launcher: 718 | dependency: "direct main" 719 | description: 720 | name: url_launcher 721 | sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" 722 | url: "https://pub.dev" 723 | source: hosted 724 | version: "6.3.1" 725 | url_launcher_android: 726 | dependency: transitive 727 | description: 728 | name: url_launcher_android 729 | sha256: "17cd5e205ea615e2c6ea7a77323a11712dffa0720a8a90540db57a01347f9ad9" 730 | url: "https://pub.dev" 731 | source: hosted 732 | version: "6.3.2" 733 | url_launcher_ios: 734 | dependency: transitive 735 | description: 736 | name: url_launcher_ios 737 | sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626" 738 | url: "https://pub.dev" 739 | source: hosted 740 | version: "6.3.2" 741 | url_launcher_linux: 742 | dependency: transitive 743 | description: 744 | name: url_launcher_linux 745 | sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" 746 | url: "https://pub.dev" 747 | source: hosted 748 | version: "3.2.1" 749 | url_launcher_macos: 750 | dependency: transitive 751 | description: 752 | name: url_launcher_macos 753 | sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" 754 | url: "https://pub.dev" 755 | source: hosted 756 | version: "3.2.2" 757 | url_launcher_platform_interface: 758 | dependency: transitive 759 | description: 760 | name: url_launcher_platform_interface 761 | sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" 762 | url: "https://pub.dev" 763 | source: hosted 764 | version: "2.3.2" 765 | url_launcher_web: 766 | dependency: transitive 767 | description: 768 | name: url_launcher_web 769 | sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" 770 | url: "https://pub.dev" 771 | source: hosted 772 | version: "2.3.3" 773 | url_launcher_windows: 774 | dependency: transitive 775 | description: 776 | name: url_launcher_windows 777 | sha256: "44cf3aabcedde30f2dba119a9dea3b0f2672fbe6fa96e85536251d678216b3c4" 778 | url: "https://pub.dev" 779 | source: hosted 780 | version: "3.1.3" 781 | uuid: 782 | dependency: transitive 783 | description: 784 | name: uuid 785 | sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff 786 | url: "https://pub.dev" 787 | source: hosted 788 | version: "4.5.1" 789 | vector_math: 790 | dependency: transitive 791 | description: 792 | name: vector_math 793 | sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" 794 | url: "https://pub.dev" 795 | source: hosted 796 | version: "2.1.4" 797 | vibration: 798 | dependency: "direct main" 799 | description: 800 | name: vibration 801 | sha256: "06588a845a4ebc73ab7ff7da555c2b3dbcd9676164b5856a38bf0b2287f1045d" 802 | url: "https://pub.dev" 803 | source: hosted 804 | version: "1.9.0" 805 | vibration_platform_interface: 806 | dependency: transitive 807 | description: 808 | name: vibration_platform_interface 809 | sha256: "6ffeee63547562a6fef53c05a41d4fdcae2c0595b83ef59a4813b0612cd2bc36" 810 | url: "https://pub.dev" 811 | source: hosted 812 | version: "0.0.3" 813 | vm_service: 814 | dependency: transitive 815 | description: 816 | name: vm_service 817 | sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 818 | url: "https://pub.dev" 819 | source: hosted 820 | version: "13.0.0" 821 | web: 822 | dependency: transitive 823 | description: 824 | name: web 825 | sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" 826 | url: "https://pub.dev" 827 | source: hosted 828 | version: "0.5.1" 829 | win32: 830 | dependency: transitive 831 | description: 832 | name: win32 833 | sha256: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb" 834 | url: "https://pub.dev" 835 | source: hosted 836 | version: "5.5.0" 837 | win32_registry: 838 | dependency: transitive 839 | description: 840 | name: win32_registry 841 | sha256: "10589e0d7f4e053f2c61023a31c9ce01146656a70b7b7f0828c0b46d7da2a9bb" 842 | url: "https://pub.dev" 843 | source: hosted 844 | version: "1.1.3" 845 | xdg_directories: 846 | dependency: transitive 847 | description: 848 | name: xdg_directories 849 | sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" 850 | url: "https://pub.dev" 851 | source: hosted 852 | version: "1.1.0" 853 | xml: 854 | dependency: transitive 855 | description: 856 | name: xml 857 | sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 858 | url: "https://pub.dev" 859 | source: hosted 860 | version: "6.5.0" 861 | yaml: 862 | dependency: transitive 863 | description: 864 | name: yaml 865 | sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" 866 | url: "https://pub.dev" 867 | source: hosted 868 | version: "3.1.2" 869 | sdks: 870 | dart: ">=3.3.4 <4.0.0" 871 | flutter: ">=3.19.0" 872 | --------------------------------------------------------------------------------