Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds photo support #126

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CONTRIBUTORS.md
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@
- Harry Schiller (@waitingwittykitty)
- David Coker (@daoxve)
- Adrasteon (@AdrasteonDev)
- Alberto Salguero (@agsalguero)

## Testing & Feedback
- Augusto Vesco
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions lib/lang/en.dart
Original file line number Diff line number Diff line change
@@ -209,4 +209,7 @@ const Map<String, String> 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.',
};
5 changes: 4 additions & 1 deletion lib/lang/es.dart
Original file line number Diff line number Diff line change
@@ -208,5 +208,8 @@ const Map<String, String> 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.',
};
4 changes: 2 additions & 2 deletions lib/pages/home/calendar_editor/calendar_editor_page.dart
Original file line number Diff line number Diff line change
@@ -200,15 +200,15 @@ class _CalendarEditorPageState extends State<CalendarEditorPage> {
context,
pickerConfig: AssetPickerConfig(
maxAssets: 1,
requestType: RequestType.video,
requestType: RequestType.common,
filterOptions: shouldIgnoreFilter ? null : filterOptionGroup,
sortPathsByModifiedDate: true,
specialItemPosition: SpecialItemPosition.prepend,
specialItemBuilder: (context, path, length) {
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(
46 changes: 46 additions & 0 deletions lib/pages/home/settings/widgets/preferences_page.dart
Original file line number Diff line number Diff line change
@@ -16,6 +16,7 @@ class _PreferencesPageState extends State<PreferencesPage> {
late bool isPickerSwitchToggled;
late bool isPickerFilterSwitchToggled;
late bool isColorsSwitchToggled;
late bool isExtendedQuickCutsSwitchToggled;

@override
void initState() {
@@ -24,6 +25,7 @@ class _PreferencesPageState extends State<PreferencesPage> {
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<PreferencesPage> {
),
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),
],
),
),
147 changes: 113 additions & 34 deletions lib/pages/save_video/save_video_page.dart
Original file line number Diff line number Diff line change
@@ -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<SaveVideoPage> {
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<SaveVideoPage> {
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<SaveVideoPage> {
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<SaveVideoPage> {

@override
Widget build(BuildContext context) {
const editorProperties = TrimEditorProperties();
final List<double>quickCutNumbers = (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<SaveVideoPage> {
),
child: SaveButton(
videoPath: _tempVideoPath,
videoController: _trimmer.videoPlayerController!,
dateColor: currentColor,
dateFormat: _dateFinalFormatValueForVideoEdit,
isTextDate: isTextDate,
@@ -526,13 +549,13 @@ class _SaveVideoPageState extends State<SaveVideoPage> {
: 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<SaveVideoPage> {
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<Color>(
editorProperties.quickCutForegroundColor,
),
backgroundColor: MaterialStateProperty.all<Color>(
editorProperties.quickCutBackgroundColor,
),
overlayColor: MaterialStateProperty.all<Color>(
editorProperties.quickCutForegroundColor.withOpacity(0.25),
),
shape: MaterialStateProperty.all<RoundedRectangleBorder>(
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<SaveVideoPage> {
}

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';
}
}
49 changes: 30 additions & 19 deletions lib/pages/save_video/widgets/save_button.dart
Original file line number Diff line number Diff line change
@@ -19,40 +19,38 @@ 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,
required this.videoStartInMilliseconds,
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;
final double videoStartInMilliseconds;
final double videoEndInMilliseconds;
final DateTime determinedDate;
final bool isFromRecordingPage;
final bool isImage;

@override
_SaveButtonState createState() => _SaveButtonState();
@@ -260,26 +258,34 @@ class _SaveButtonState extends State<SaveButton> {
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)
String audioStream = '';
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,15 +319,20 @@ class _SaveButtonState extends State<SaveButton> {
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 =
'scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2:black';

// 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<SaveButton> {

// 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');

6 changes: 3 additions & 3 deletions pubspec.lock
Original file line number Diff line number Diff line change
@@ -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: