diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index dc445d1..56f1744 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -8,6 +8,7 @@ - Harry Schiller (@waitingwittykitty) - David Coker (@daoxve) - Adrasteon (@AdrasteonDev) +- Alberto Salguero (@agsalguero) ## Testing & Feedback - Augusto Vesco diff --git a/README.md b/README.md index a3b640e..66ccc54 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ Then, after a couple months or even years: ## Features - Record up to 10 seconds of video (1080p resolution) -- Pick videos from gallery +- Pick videos or photos from gallery - Add or edit subtitles in the videos - Add automatic or manual geotagging on top of the videos - Choose the date format and color to show on top of the videos diff --git a/lib/lang/en.dart b/lib/lang/en.dart index 47ed045..4707e35 100644 --- a/lib/lang/en.dart +++ b/lib/lang/en.dart @@ -209,4 +209,7 @@ const Map en = { 'useAlternativeCalendarColors': 'Use alternative calendar colors', 'useAlternativeCalendarColorsDescription': 'Changes green and red in calendar to blue and yellow. Useful for colorblind people.', + 'useExtendedQuickCuts': 'Use extended quickcuts', + 'useExtendedQuickCutsDescription': + 'Add more duration values for cutting videos. Useful for cutting videos with more precision.', }; diff --git a/lib/lang/es.dart b/lib/lang/es.dart index 6750222..821b6fc 100644 --- a/lib/lang/es.dart +++ b/lib/lang/es.dart @@ -208,5 +208,8 @@ const Map es = { 'Cuando está activado, seleccionar fechas pasadas filtrará los vídeos por esa fecha. Si está desactivado, se mostrarán todos los vídeos. Funciona solo con el selector de archivos experimental.', 'useAlternativeCalendarColors': 'Use colores alternativos para el calendario', 'useAlternativeCalendarColorsDescription': - 'Cambia el verde y el rojo en el calendario a azul y amarillo. Útil para personas con daltonismo.' + 'Cambia el verde y el rojo en el calendario a azul y amarillo. Útil para personas con daltonismo.', + 'useExtendedQuickCuts': 'Usar botones de corte extendidos', + 'useExtendedQuickCutsDescription': + 'Añade más botones de duración para cortar los vídeos. Útil para recortar los vídeos con mayor precisión.', }; diff --git a/lib/pages/home/calendar_editor/calendar_editor_page.dart b/lib/pages/home/calendar_editor/calendar_editor_page.dart index 45f30c3..8cd46e8 100644 --- a/lib/pages/home/calendar_editor/calendar_editor_page.dart +++ b/lib/pages/home/calendar_editor/calendar_editor_page.dart @@ -200,7 +200,7 @@ class _CalendarEditorPageState extends State { context, pickerConfig: AssetPickerConfig( maxAssets: 1, - requestType: RequestType.video, + requestType: RequestType.common, filterOptions: shouldIgnoreFilter ? null : filterOptionGroup, sortPathsByModifiedDate: true, specialItemPosition: SpecialItemPosition.prepend, @@ -208,7 +208,7 @@ class _CalendarEditorPageState extends State { return Center( child: Text( shouldIgnoreFilter - ? 'Latest\nvideos' + ? 'Latest\nmedia' : 'From\n${_selectedDate.toString().substring(0, 10).split('-').reversed.join('-')}\nonwards', textAlign: TextAlign.center, style: const TextStyle( diff --git a/lib/pages/home/settings/widgets/preferences_page.dart b/lib/pages/home/settings/widgets/preferences_page.dart index cfb052a..60e249d 100644 --- a/lib/pages/home/settings/widgets/preferences_page.dart +++ b/lib/pages/home/settings/widgets/preferences_page.dart @@ -16,6 +16,7 @@ class _PreferencesPageState extends State { late bool isPickerSwitchToggled; late bool isPickerFilterSwitchToggled; late bool isColorsSwitchToggled; + late bool isExtendedQuickCutsSwitchToggled; @override void initState() { @@ -24,6 +25,7 @@ class _PreferencesPageState extends State { isPickerSwitchToggled = SharedPrefsUtil.getBool('useExperimentalPicker') ?? true; isPickerFilterSwitchToggled = SharedPrefsUtil.getBool('useFilterInExperimentalPicker') ?? false; isColorsSwitchToggled = SharedPrefsUtil.getBool('useAlternativeCalendarColors') ?? false; + isExtendedQuickCutsSwitchToggled = SharedPrefsUtil.getBool('useExtendedQuickCuts') ?? false; } @override @@ -229,6 +231,50 @@ class _PreferencesPageState extends State { ), const SizedBox(height: 5.0), Text('useAlternativeCalendarColorsDescription'.tr), + const Divider(), + Container( + padding: const EdgeInsets.symmetric(horizontal: 15.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text( + 'useExtendedQuickCuts'.tr, + style: TextStyle( + fontSize: MediaQuery.of(context).size.width * 0.045, + ), + ), + ), + Switch( + value: isExtendedQuickCutsSwitchToggled, + onChanged: (value) async { + if (value) { + Utils.logInfo( + '[PREFERENCES] - Use extended quickCuts was enabled', + ); + + SharedPrefsUtil.putBool('useExtendedQuickCuts', true); + } else { + Utils.logInfo( + '[PREFERENCES] - Use extended quickCuts was disabled', + ); + + SharedPrefsUtil.putBool('useExtendedQuickCuts', false); + } + + /// Update switch value + setState(() { + isExtendedQuickCutsSwitchToggled = !isExtendedQuickCutsSwitchToggled; + }); + }, + activeTrackColor: AppColors.mainColor.withOpacity(0.4), + activeColor: AppColors.mainColor, + ), + ], + ), + ), + const SizedBox(height: 5.0), + Text('useExtendedQuickCutsDescription'.tr), ], ), ), diff --git a/lib/pages/save_video/save_video_page.dart b/lib/pages/save_video/save_video_page.dart index 64e697b..49301f1 100644 --- a/lib/pages/save_video/save_video_page.dart +++ b/lib/pages/save_video/save_video_page.dart @@ -7,6 +7,7 @@ import 'package:geolocator/geolocator.dart'; import 'package:get/get.dart'; import 'package:group_radio_button/group_radio_button.dart'; import 'package:image_picker/image_picker.dart'; +import 'package:mime/mime.dart'; import 'package:video_trimmer/video_trimmer.dart'; import '../../controllers/recording_settings_controller.dart'; @@ -64,6 +65,7 @@ class _SaveVideoPageState extends State { double _videoEndValue = 0.0; bool _isVideoPlaying = false; bool _isLocationProcessing = false; + bool _isImage = false; late final bool isDarkTheme = ThemeService().isDarkTheme(); String selectedProfileName = Utils.getCurrentProfile(); @@ -294,9 +296,14 @@ class _SaveVideoPageState extends State { pickerColor = parseColorString(_recordingSettingsController.dateColor.value); currentColor = pickerColor; _tempVideoPath = routeArguments['videoPath']; + _isImage = isImage(_tempVideoPath); isTextDate = _recordingSettingsController.dateFormatId.value == 1; _initCorrectDates(); - _initVideoPlayerController(); + if(_isImage) { + _videoEndValue = 1.0; + } else { + _initVideoPlayerController(); + } if (isGeotaggingEnabled) { setGeotagging(); } @@ -367,17 +374,29 @@ class _SaveVideoPageState extends State { return ColoredBox( color: AppColors.dark, child: GestureDetector( - onTap: () => videoPlay(), + onTap: _isImage ? null : () => videoPlay(), // @todo animate effect for images child: AspectRatio( aspectRatio: 16 / 9, child: Stack( children: [ - VideoViewer( - trimmer: _trimmer, - ), + if (_isImage) + SizedBox( + width: MediaQuery.of(context).size.width, + child: ColoredBox( + color: Colors.black, + child: Image.file( + File(_tempVideoPath), + fit: BoxFit.contain, + ), + ), + ) + else + VideoViewer( + trimmer: _trimmer, + ), Center( child: Opacity( - opacity: _isVideoPlaying ? 0.0 : 1.0, + opacity: _isVideoPlaying || _isImage ? 0.0 : 1.0, // @todo show play button for images child: Container( width: MediaQuery.of(context).size.width * 0.25, height: MediaQuery.of(context).size.width * 0.25, @@ -465,6 +484,11 @@ class _SaveVideoPageState extends State { @override Widget build(BuildContext context) { + const editorProperties = TrimEditorProperties(); + final ListquickCutNumbers = (SharedPrefsUtil.getBool('useExtendedQuickCuts') ?? false) + ? [1, 1.5, 2, 3, 4, 5, 6, 7, 8, 9, 10] + : [1, 2, 3, 5, 10]; + return PopScope( canPop: false, onPopInvoked: (_) async { @@ -516,7 +540,6 @@ class _SaveVideoPageState extends State { ), child: SaveButton( videoPath: _tempVideoPath, - videoController: _trimmer.videoPlayerController!, dateColor: currentColor, dateFormat: _dateFinalFormatValueForVideoEdit, isTextDate: isTextDate, @@ -526,13 +549,13 @@ class _SaveVideoPageState extends State { : customLocationTextController.text, subtitles: _subtitles, videoStartInMilliseconds: _videoStartValue, - videoEndInMilliseconds: getVideoEndInMilliseconds(), - videoDuration: _trimmer.videoPlayerController!.value.duration.inSeconds, + videoEndInMilliseconds: _isImage ? _videoEndValue * 1000 : getVideoEndInMilliseconds(), isGeotaggingEnabled: isGeotaggingEnabled, textOutlineColor: invert(currentColor), textOutlineWidth: textOutlineStrokeWidth, determinedDate: routeArguments['currentDate'], isFromRecordingPage: routeArguments['isFromRecordingPage'], + isImage: _isImage, ), ), body: Column( @@ -546,31 +569,78 @@ class _SaveVideoPageState extends State { Center( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 6.0), - child: TrimViewer( - trimmer: _trimmer, - viewerHeight: 50.0, - type: ViewerType.fixed, - editorProperties: TrimEditorProperties( - borderWidth: 2.5, - circleSize: 6.0, - circleSizeOnDrag: 9.0, - circlePaintColor: isDarkTheme ? Colors.white : AppColors.mainColor, - borderPaintColor: - isDarkTheme ? AppColors.light : AppColors.mainColor.withOpacity(0.75), - quickCutBackgroundColor: isDarkTheme - ? AppColors.light.withOpacity(0.15) - : AppColors.dark.withOpacity(0.40), + child: _isImage + ? SingleChildScrollView( // add quick cut buttons for images + scrollDirection: Axis.horizontal, + physics: const BouncingScrollPhysics(), + child: Row( + children: [ + for (double i in quickCutNumbers) + Padding( + padding: const EdgeInsets.all(10.0), + child: TextButton.icon( + style: ButtonStyle( + foregroundColor: MaterialStateProperty.all( + editorProperties.quickCutForegroundColor, + ), + backgroundColor: MaterialStateProperty.all( + editorProperties.quickCutBackgroundColor, + ), + overlayColor: MaterialStateProperty.all( + editorProperties.quickCutForegroundColor.withOpacity(0.25), + ), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(35.0), + ), + ), + ), + icon: Icon( + editorProperties.quickCutIcon, + size: editorProperties.quickCutIconSize, + ), + onPressed: () { + _videoStartValue = 0; + _videoEndValue = i; + setState(() {}); + }, + label: Text( + (i == i.roundToDouble()?i.round():i).toString(), + style: TextStyle(color: (i == _videoEndValue) + ? Colors.yellow + : editorProperties.quickCutTextColor), + ), + ), + ), + ], + ), + ) + : TrimViewer( // use video trimmer for videos + trimmer: _trimmer, + viewerHeight: 50.0, + type: ViewerType.fixed, + editorProperties: TrimEditorProperties( + borderWidth: 2.5, + circleSize: 6.0, + circleSizeOnDrag: 9.0, + circlePaintColor: isDarkTheme ? Colors.white : AppColors.mainColor, + borderPaintColor: + isDarkTheme ? AppColors.light : AppColors.mainColor.withOpacity(0.75), + quickCutBackgroundColor: isDarkTheme + ? AppColors.light.withOpacity(0.15) + : AppColors.dark.withOpacity(0.40), + ), + durationStyle: DurationStyle.FORMAT_SS_MS, + durationTextStyle: isDarkTheme + ? const TextStyle(color: Colors.white) + : const TextStyle(color: Colors.black), + maxVideoLength: const Duration(milliseconds: 10000), + viewerWidth: MediaQuery.of(context).size.width, + onChangeStart: (value) => _videoStartValue = value, + onChangeEnd: (value) => _videoEndValue = value, + onChangePlaybackState: (value) => setState(() => _isVideoPlaying = value), + quickCutNumbers: quickCutNumbers, ), - durationStyle: DurationStyle.FORMAT_SS_MS, - durationTextStyle: isDarkTheme - ? const TextStyle(color: Colors.white) - : const TextStyle(color: Colors.black), - maxVideoLength: const Duration(milliseconds: 10000), - viewerWidth: MediaQuery.of(context).size.width, - onChangeStart: (value) => _videoStartValue = value, - onChangeEnd: (value) => _videoEndValue = value, - onChangePlaybackState: (value) => setState(() => _isVideoPlaying = value), - ), ), ), ], @@ -1034,11 +1104,20 @@ class _SaveVideoPageState extends State { } double getVideoEndInMilliseconds() { - final double defaultEnd = _videoEndValue + 500; + final double defaultEnd = _videoEndValue; final int videoDuration = _trimmer.videoPlayerController!.value.duration.inMilliseconds; if (defaultEnd > videoDuration) { return videoDuration.toDouble(); } return defaultEnd; } + + bool isImage(String path) { + final String? mimeStr = lookupMimeType(path); + if (mimeStr == null) { + return false; + } + final fileType = mimeStr.split('/'); + return fileType[0] == 'image'; + } } diff --git a/lib/pages/save_video/widgets/save_button.dart b/lib/pages/save_video/widgets/save_button.dart index f5d1ca7..7dd776b 100644 --- a/lib/pages/save_video/widgets/save_button.dart +++ b/lib/pages/save_video/widgets/save_button.dart @@ -19,14 +19,12 @@ import '../../../utils/utils.dart'; class SaveButton extends StatefulWidget { SaveButton({ required this.videoPath, - required this.videoController, required this.dateColor, required this.dateFormat, required this.isTextDate, required this.userPosition, required this.userLocation, required this.subtitles, - required this.videoDuration, required this.isGeotaggingEnabled, required this.textOutlineColor, required this.textOutlineWidth, @@ -34,18 +32,17 @@ class SaveButton extends StatefulWidget { required this.videoEndInMilliseconds, required this.determinedDate, required this.isFromRecordingPage, + required this.isImage, }); // Finding controllers final String videoPath; - final VideoPlayerController videoController; final Color dateColor; final String dateFormat; final bool isTextDate; final Position? userPosition; final String? userLocation; final String? subtitles; - final int videoDuration; final bool isGeotaggingEnabled; final Color textOutlineColor; final double textOutlineWidth; @@ -53,6 +50,7 @@ class SaveButton extends StatefulWidget { final double videoEndInMilliseconds; final DateTime determinedDate; final bool isFromRecordingPage; + final bool isImage; @override _SaveButtonState createState() => _SaveButtonState(); @@ -260,7 +258,7 @@ class _SaveButtonState extends State { if (isGeotaggingEnabled) { final String locationTextFilePath = await Utils.writeLocationTxt(widget.userLocation); locale = - ', drawtext=textfile=$locationTextFilePath:fontfile=$fontPath:fontsize=$locTextSize:fontcolor=\'$parsedDateColor\':borderw=${widget.textOutlineWidth}:bordercolor=$parsedTextOutlineColor:x=$locPosX:y=$locPosY'; + ',drawtext="textfile=$locationTextFilePath:fontfile=$fontPath:fontsize=$locTextSize:fontcolor=\'$parsedDateColor\':borderw=${widget.textOutlineWidth}:bordercolor=$parsedTextOutlineColor:x=$locPosX:y=$locPosY"'; } // Check if video was added from gallery and has an audio stream, adding one if not (screen recordings can be muted for example) @@ -268,18 +266,26 @@ class _SaveButtonState extends State { String origin = 'osd_recording'; if (!widget.isFromRecordingPage) { origin = 'gallery'; - await executeFFprobe( - '-v quiet -select_streams a:0 -show_entries stream=codec_type -of default=nw=1:nk=1 "$videoPath"') - .then((session) async { - final returnCode = await session.getReturnCode(); - if (ReturnCode.isSuccess(returnCode)) { - final sessionLog = await session.getOutput(); - if (sessionLog == null || sessionLog.isEmpty) { - Utils.logWarning('${logTag}Video has no audio stream, adding one.'); - audioStream = '-f lavfi -i anullsrc=channel_layout=mono:sample_rate=48000 -shortest'; + if(widget.isImage) { + Utils.logInfo( + '${logTag}Adding audio stream to image.'); + audioStream = '-f lavfi -i anullsrc=channel_layout=mono:sample_rate=48000'; + } else { + await executeFFprobe( + '-v quiet -select_streams a:0 -show_entries stream=codec_type -of default=nw=1:nk=1 "$videoPath"') + .then((session) async { + final returnCode = await session.getReturnCode(); + if (ReturnCode.isSuccess(returnCode)) { + final sessionLog = await session.getOutput(); + if (sessionLog == null || sessionLog.isEmpty) { + Utils.logWarning( + '${logTag}Video has no audio stream, adding one.'); + audioStream = + '-f lavfi -i anullsrc=channel_layout=mono:sample_rate=48000 -shortest'; + } } - } - }); + }); + } } // If subtitles TextBox were not left empty, we can allow the command to render the subtitles into the video, otherwise we add empty subtitles to populate the streams with a subtitle stream, so that concat demuxer can work properly when creating a movie @@ -313,7 +319,12 @@ class _SaveButtonState extends State { final metadata = baseMetadata + locationMetadata; // Trim video to the selected range - final trim = '-ss ${videoStartInMilliseconds}ms -to ${videoEndInMilliseconds}ms'; + final trim = widget.isImage + ? '-loop 1 -t ${videoEndInMilliseconds}ms' + : '-ss ${videoStartInMilliseconds}ms -to ${videoEndInMilliseconds}ms'; + + const finalZoom = 0.1; + final imageEffect = widget.isImage ? ',zoompan=z=\'1+${finalZoom}*in_time/${videoEndInMilliseconds/1000}\':d=1' : ''; // Scale video to 1920x1080 and add black padding if needed const scale = @@ -321,7 +332,7 @@ class _SaveButtonState extends State { // Add date to the video final date = - ',drawtext="$fontPath:text=\'${widget.dateFormat}\':fontsize=$dateTextSize:fontcolor=\'$parsedDateColor\':borderw=${widget.textOutlineWidth}:bordercolor=$parsedTextOutlineColor:x=$datePosX:y=$datePosY'; + ',drawtext="$fontPath:text=\'${widget.dateFormat}\':fontsize=$dateTextSize:fontcolor=\'$parsedDateColor\':borderw=${widget.textOutlineWidth}:bordercolor=$parsedTextOutlineColor:x=$datePosX:y=$datePosY"'; // Add subtitles to the video const subtitles = '-c:s mov_text -map 1:v -map 1:a? -map 0:s -disposition:s:0 default'; @@ -332,7 +343,7 @@ class _SaveButtonState extends State { // Full command to edit and save video final command = - '-i "$subtitlesPath" $trim -i "$videoPath" $audioStream $metadata -vf [in]$scale$date$locale[out]" $defaultEditSettings $subtitles "$finalPath" -y'; + '-i "$subtitlesPath" $trim -i "$videoPath" $audioStream $metadata -vf [in]$scale$imageEffect$date$locale[out] $defaultEditSettings $subtitles "$finalPath" -y'; Utils.logInfo('${logTag}FFmpeg full command: $command'); diff --git a/pubspec.lock b/pubspec.lock index 2ae9098..db8f68c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1227,10 +1227,10 @@ packages: description: path: "." ref: main - resolved-ref: "3311af83faf02aa4eb87ebc6b02fb6c682256e3a" - url: "https://github.com/KyleKun/video_trimmer.git" + resolved-ref: "8e3e5f5227029609e0bce982332dc2381c83300d" + url: "https://github.com/agsalguero/video_trimmer.git" source: git - version: "2.1.6" + version: "2.1.7" wakelock_plus: dependency: "direct main" description: