diff --git a/lib/lang/de.dart b/lib/lang/de.dart index bbf7319..62e732c 100644 --- a/lib/lang/de.dart +++ b/lib/lang/de.dart @@ -50,8 +50,7 @@ const Map de = { 'introTitle2': 'Erstelle den Film Deines Lebens', 'introDesc2': 'Kreiere eine Zusammenstellung Deiner schönsten Momente.', 'introTitle3': 'Keine Werbung und kostenlos', - 'introDesc3': - 'Wenn Du die App magst, freuen wir uns auf eine Spende.', + 'introDesc3': 'Wenn Du die App magst, freuen wir uns auf eine Spende.', 'skip': 'Überspringen', 'done': 'Fertig', 'futureUpdates': 'Zukünftige Updates', @@ -83,12 +82,13 @@ const Map de = { 'allTime': 'Alle zeit', 'last7Days': 'Letzten 7 tage', 'last30Days': 'Letzte 30 tage', - 'last60Days':'Letzte 60 tage', - 'last90Days':'Letzte 90 tage', + 'last60Days': 'Letzte 60 tage', + 'last90Days': 'Letzte 90 tage', 'thisMonth': 'Diesen monat', 'thisYear': 'Dieses jahr', 'lastYear': 'Letztes jahr', 'custom': 'Brauch', 'dateRange': 'Datumsbereich', 'clipsFound': 'Clips gefunden', + 'enterSubtitles': 'Untertitel eingeben', }; diff --git a/lib/lang/en.dart b/lib/lang/en.dart index 359268e..dcb62b5 100644 --- a/lib/lang/en.dart +++ b/lib/lang/en.dart @@ -83,12 +83,13 @@ const Map en = { 'allTime': 'All Time', 'last7Days': 'Last 7 days', 'last30Days': 'Last 30 days', - 'last60Days':'Last 60 days', - 'last90Days':'Last 90 days', + 'last60Days': 'Last 60 days', + 'last90Days': 'Last 90 days', 'thisMonth': 'This month', 'thisYear': 'This year', 'lastYear': 'Last year', 'custom': 'Custom', 'dateRange': 'Date Range', 'clipsFound': 'Clips Found', + 'enterSubtitles': 'Enter subtitles', }; diff --git a/lib/lang/es.dart b/lib/lang/es.dart index 41b1791..5f47a30 100644 --- a/lib/lang/es.dart +++ b/lib/lang/es.dart @@ -82,12 +82,13 @@ const Map es = { 'allTime': 'Todo el tiempo', 'last7Days': 'Los últimos 7 días', 'last30Days': 'Últimos 30 días', - 'last60Days':'Últimos 60 días', - 'last90Days':'Últimos 90 días', + 'last60Days': 'Últimos 60 días', + 'last90Days': 'Últimos 90 días', 'thisMonth': 'Este mes', 'thisYear': 'Este año', 'lastYear': 'El año pasado', 'custom': 'Disfraz', 'dateRange': 'Rango de fechas', 'clipsFound': 'Clips encontrados', + 'enterSubtitles': 'Introducir subtítulos', }; diff --git a/lib/lang/fr.dart b/lib/lang/fr.dart index 60370ed..aa74518 100644 --- a/lib/lang/fr.dart +++ b/lib/lang/fr.dart @@ -84,12 +84,13 @@ const Map fr = { 'allTime': 'Tout le temps', 'last7Days': 'Les 7 derniers jours', 'last30Days': 'Les 30 derniers jours', - 'last60Days':'60 derniers jours', - 'last90Days':'90 derniers jours', + 'last60Days': '60 derniers jours', + 'last90Days': '90 derniers jours', 'thisMonth': 'Ce mois-ci', 'thisYear': 'Cette année', 'lastYear': 'L\'année dernière', 'custom': 'Personnalisé', 'dateRange': 'Date Range', 'clipsFound': 'Clips trouvés', + 'enterSubtitles': 'Entrer le sous-titre', }; diff --git a/lib/lang/id.dart b/lib/lang/id.dart index 8f914ef..70c4caa 100644 --- a/lib/lang/id.dart +++ b/lib/lang/id.dart @@ -15,7 +15,8 @@ const Map id = { 'movieInsufficientVideos': 'Anda harus memiliki lebih 2 rekaman video untuk bisa membuat video gabungan', 'movieCreatedTitle': 'Video dibuat!', - 'movieCreatedDesc': 'Video disimpan ke dalam penyimpanan di dalam folder OSD-Movies folder', + 'movieCreatedDesc': + 'Video disimpan ke dalam penyimpanan di dalam folder OSD-Movies folder', 'movieError': 'Gagal menyimpan video!', 'tryAgainMsg': 'Coba beberapa saaat lagi. Jika masalah masih tetap ada, hubungi pengembang.', @@ -26,8 +27,7 @@ const Map id = { 'tapBelowToGenerate': 'Tekan tombol di bawah untuk menghasilkan\n satu file video single video file:', 'editQuestionTitle': 'Ubah video?', - 'editQuestion': - 'Rekaman sebelumnya akan dihapus, apakah Anda yakin?', + 'editQuestion': 'Rekaman sebelumnya akan dihapus, apakah Anda yakin?', 'yes': 'Ya', 'no': 'Tidak', 'edit': 'Ubah', @@ -83,12 +83,13 @@ const Map id = { 'allTime': 'Sepanjang waktu', 'last7Days': '7 hari terakhir', 'last30Days': '30 hari terakhir', - 'last60Days':'60 hari terakhir', - 'last90Days':'90 hari terakhir', + 'last60Days': '60 hari terakhir', + 'last90Days': '90 hari terakhir', 'thisMonth': 'Bulan ini', 'thisYear': 'Tahun ini', 'lastYear': 'Tahun lalu', 'custom': 'Kebiasaan', 'dateRange': 'Rentang tanggal', 'clipsFound': 'Klip ditemukan', + 'enterSubtitles': 'Masukkan subjudul', }; diff --git a/lib/lang/pt.dart b/lib/lang/pt.dart index 12d8bc1..78ac900 100644 --- a/lib/lang/pt.dart +++ b/lib/lang/pt.dart @@ -82,12 +82,13 @@ const Map pt = { 'allTime': 'Tempo todo', 'last7Days': 'Últimos 7 dias', 'last30Days': 'Últimos 30 dias', - 'last60Days':'Últimos 60 dias', - 'last90Days':'Últimos 90 dias', + 'last60Days': 'Últimos 60 dias', + 'last90Days': 'Últimos 90 dias', 'thisMonth': 'Este mês', 'thisYear': 'Este ano', 'lastYear': 'Ano passado', - 'custom': 'Personalizada', - 'dateRange': 'DATA DE DATA', + 'custom': 'Personalizado', + 'dateRange': 'Período', 'clipsFound': 'Clipes encontrados', + 'enterSubtitles': 'Digite uma legenda', }; diff --git a/lib/lang/zh.dart b/lib/lang/zh.dart index 375c74c..5db5791 100644 --- a/lib/lang/zh.dart +++ b/lib/lang/zh.dart @@ -76,12 +76,13 @@ const Map zh = { 'allTime': '整天', 'last7Days': '最近7天', 'last30Days': '最近30天', - 'last60Days':'最近60天', - 'last90Days':'最近90天', + 'last60Days': '最近60天', + 'last90Days': '最近90天', 'thisMonth': '这个月', 'thisYear': '今年', 'lastYear': '去年', 'custom': '风俗', 'dateRange': '日期范围', 'clipsFound': '找到夹子', + 'enterSubtitles': '输入字幕', }; diff --git a/lib/pages/save_video/save_video_page.dart b/lib/pages/save_video/save_video_page.dart index 74180bd..31b8818 100644 --- a/lib/pages/save_video/save_video_page.dart +++ b/lib/pages/save_video/save_video_page.dart @@ -27,7 +27,8 @@ class _SaveVideoPageState extends State { late String _tempVideoPath; late VideoPlayerController _videoController; - final TextEditingController customLocationTextController = TextEditingController(); + final TextEditingController customLocationTextController = + TextEditingController(); Color pickerColor = const Color(0xff000000); Color currentColor = const Color(0xff000000); @@ -46,10 +47,13 @@ class _SaveVideoPageState extends State { String? _currentAddress; Position? _currentPosition; - bool isGeotaggingEnabled = SharedPrefsUtil.getBool('isGeotaggingEnabled') ?? false; + bool isGeotaggingEnabled = + SharedPrefsUtil.getBool('isGeotaggingEnabled') ?? false; + String? _subtitles; Future checkGeotaggingStatus() async { - isGeotaggingEnabled = SharedPrefsUtil.getBool('isGeotaggingEnabled') ?? false; + isGeotaggingEnabled = + SharedPrefsUtil.getBool('isGeotaggingEnabled') ?? false; if (isGeotaggingEnabled) { await _getCurrentPosition(); } @@ -68,15 +72,16 @@ class _SaveVideoPageState extends State { serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar( - content: Text('Location services are disabled. Please enable the services'))); + content: Text( + 'Location services are disabled. Please enable the services'))); return false; } permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); if (permission == LocationPermission.denied) { - ScaffoldMessenger.of(context) - .showSnackBar(const SnackBar(content: Text('Location permissions are denied'))); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Location permissions are denied'))); return false; } } @@ -103,7 +108,8 @@ class _SaveVideoPageState extends State { } Future _getAddressFromLatLng(Position position) async { - await placemarkFromCoordinates(_currentPosition!.latitude, _currentPosition!.longitude) + await placemarkFromCoordinates( + _currentPosition!.latitude, _currentPosition!.longitude) .then((List placemarks) { final Placemark place = placemarks[0]; setState(() { @@ -269,7 +275,8 @@ class _SaveVideoPageState extends State { children: [ Text( customLocationTextController.text.isEmpty - ? _currentAddress ?? customLocationTextController.text + ? _currentAddress ?? + customLocationTextController.text : customLocationTextController.text, style: TextStyle( fontSize: MediaQuery.of(context).size.width * 0.032, @@ -281,7 +288,8 @@ class _SaveVideoPageState extends State { ), Text( customLocationTextController.text.isEmpty - ? _currentAddress ?? customLocationTextController.text + ? _currentAddress ?? + customLocationTextController.text : customLocationTextController.text, style: TextStyle( fontSize: MediaQuery.of(context).size.width * 0.032, @@ -325,224 +333,233 @@ class _SaveVideoPageState extends State { appBar: AppBar( title: Text('saveVideo'.tr), ), - body: Column( - children: [ - _dailyVideoPlayer(), - const Spacer(), - videoProperties(), - const Spacer(flex: 2), - SaveButton( - videoPath: _tempVideoPath, - videoController: _videoController, - dateColor: currentColor, - dateFormat: _dateFormatValue, - isTextDate: isTextDate, - userLocation: customLocationTextController.text.isEmpty - ? _currentAddress ?? '' - : customLocationTextController.text, - isGeotaggingEnabled: isGeotaggingEnabled, - textOutlineColor: invert(currentColor), - textOutlineWidth: textOutlineStrokeWidth, + floatingActionButton: SaveButton( + videoPath: _tempVideoPath, + videoController: _videoController, + dateColor: currentColor, + dateFormat: _dateFormatValue, + isTextDate: isTextDate, + userLocation: customLocationTextController.text.isEmpty + ? _currentAddress ?? '' + : customLocationTextController.text, + subtitles: _subtitles, + videoDuration: _videoController.value.duration.inSeconds, + isGeotaggingEnabled: isGeotaggingEnabled, + textOutlineColor: invert(currentColor), + textOutlineWidth: textOutlineStrokeWidth, + ), + body: SingleChildScrollView( + child: SizedBox( + height: MediaQuery.of(context).size.height, + child: Column( + children: [ + _dailyVideoPlayer(), + Expanded(child: videoProperties()), + ], ), - const Spacer(), - ], + ), ), ), ); } Widget videoProperties() { - return Container( - width: MediaQuery.of(context).size.width * 0.9, - height: MediaQuery.of(context).size.height * 0.49, - decoration: BoxDecoration( - border: Border.all(color: AppColors.mainColor), - borderRadius: BorderRadius.circular(30), - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - SizedBox(height: MediaQuery.of(context).size.height * 0.02), - Center( - child: Text( - 'editVideoProperties'.tr, - style: TextStyle( - fontSize: MediaQuery.of(context).size.height * 0.025, - fontWeight: FontWeight.bold, - ), + return Column( + children: [ + SizedBox(height: MediaQuery.of(context).size.height * 0.02), + Center( + child: Text( + 'editVideoProperties'.tr, + style: TextStyle( + fontSize: MediaQuery.of(context).size.height * 0.020, + fontWeight: FontWeight.bold, ), ), - SizedBox(height: MediaQuery.of(context).size.height * 0.02), - SizedBox( - height: MediaQuery.of(context).size.width * 0.8, - // decoration: BoxDecoration( - // border: Border.all(color: AppColors.mainColor), - // borderRadius: BorderRadius.circular(30), - // ), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - // Date color - GestureDetector( - onTap: () => colorPickerDialog(), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Padding( - padding: EdgeInsets.only( - left: MediaQuery.of(context).size.width * 0.04, - bottom: MediaQuery.of(context).size.height * 0.02, - ), - child: Text( - 'dateColor'.tr, - style: TextStyle( - fontSize: MediaQuery.of(context).size.height * 0.025, - ), - ), - ), - Padding( - padding: - EdgeInsets.only(left: MediaQuery.of(context).size.width * 0.04), - child: Container( - decoration: BoxDecoration( - shape: BoxShape.circle, - color: currentColor, - ), - width: MediaQuery.of(context).size.width * 0.08, - height: MediaQuery.of(context).size.width * 0.08, - ), - ), - ], + ), + SizedBox(height: MediaQuery.of(context).size.height * 0.02), + + // Date color + GestureDetector( + onTap: () => colorPickerDialog(), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only( + left: MediaQuery.of(context).size.width * 0.04, + bottom: MediaQuery.of(context).size.height * 0.02, + ), + child: Text( + 'dateColor'.tr, + style: TextStyle( + fontSize: MediaQuery.of(context).size.height * 0.020, ), ), + ), + Padding( + padding: EdgeInsets.only( + left: MediaQuery.of(context).size.width * 0.04), + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: currentColor, + ), + width: MediaQuery.of(context).size.width * 0.08, + height: MediaQuery.of(context).size.width * 0.08, + ), + ), + ], + ), + ), - // Date Format - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceEvenly, + // Date Format + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Padding( + padding: EdgeInsets.only( + left: MediaQuery.of(context).size.width * 0.04, + ), + child: Text( + 'dateFormat'.tr, + style: TextStyle( + fontSize: MediaQuery.of(context).size.height * 0.020, + ), + ), + ), + Padding( + padding: EdgeInsets.all( + MediaQuery.of(context).size.height * 0.01, + ), + child: RadioGroup.builder( + direction: Axis.vertical, + horizontalAlignment: MainAxisAlignment.start, + groupValue: _dateFormatValue, + onChanged: (value) => setState(() { + _dateFormatValue = value!; + // Place date in the bottom if it is text format + _dateFormatValue == _dateFormats[0] + ? isTextDate = false + : isTextDate = true; + }), + items: _dateFormats, + itemBuilder: (item) => RadioButtonBuilder( + item, + ), + ), + ), + ], + ), + + // Geotagging + CustomCheckboxListTile( + isChecked: isGeotaggingEnabled, + onChanged: (_) async { + await toggleGeotaggingStatus(); + if (isGeotaggingEnabled) { + print('Getting location'); + await _getCurrentPosition(); + } + setState(() {}); + }, + padding: EdgeInsets.symmetric( + horizontal: MediaQuery.of(context).size.width * 0.04), + title: Text( + 'enableGeotagging'.tr, + style: TextStyle( + fontSize: MediaQuery.of(context).size.height * 0.020, + ), + ), + ), + + ListTile( + onTap: () async { + await showDialog( + context: context, + builder: (context) => AlertDialog( + title: Center( + child: Text('setCustomLocation'.tr), + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + content: Column( + mainAxisSize: MainAxisSize.min, children: [ - Padding( - padding: EdgeInsets.only( - left: MediaQuery.of(context).size.width * 0.04, - ), - child: Text( - 'dateFormat'.tr, - style: TextStyle( - fontSize: MediaQuery.of(context).size.height * 0.025, + TextField( + controller: customLocationTextController, + cursorColor: Colors.green, + decoration: InputDecoration( + hintText: 'enterLocation'.tr, + filled: true, + border: const OutlineInputBorder( + borderSide: BorderSide(color: Colors.green), ), - ), - ), - Padding( - padding: EdgeInsets.all( - MediaQuery.of(context).size.height * 0.01, - ), - child: RadioGroup.builder( - direction: Axis.vertical, - horizontalAlignment: MainAxisAlignment.start, - groupValue: _dateFormatValue, - onChanged: (value) => setState(() { - _dateFormatValue = value!; - // Place date in the bottom if it is text format - _dateFormatValue == _dateFormats[0] - ? isTextDate = false - : isTextDate = true; - }), - items: _dateFormats, - itemBuilder: (item) => RadioButtonBuilder( - item, + enabledBorder: InputBorder.none, + focusedBorder: const OutlineInputBorder( + borderSide: BorderSide(color: Colors.green), ), ), ), ], ), - - // Geotagging - CustomCheckboxListTile( - isChecked: isGeotaggingEnabled, - onChanged: (_) async { - await toggleGeotaggingStatus(); - if (isGeotaggingEnabled) { - print('Getting location'); - await _getCurrentPosition(); - } - setState(() {}); - }, - padding: EdgeInsets.symmetric( - horizontal: MediaQuery.of(context).size.width * 0.04), - title: Text( - 'enableGeotagging'.tr, - style: TextStyle( - fontSize: MediaQuery.of(context).size.height * 0.025, + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + style: TextButton.styleFrom( + foregroundColor: AppColors.green, ), + child: Text('ok'.tr), ), - ), - - ListTile( - onTap: () async { - await showDialog( - context: context, - builder: (context) => AlertDialog( - title: Center( - child: Text('setCustomLocation'.tr), - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextField( - controller: customLocationTextController, - cursorColor: Colors.green, - decoration: InputDecoration( - hintText: 'enterLocation'.tr, - filled: true, - border: const OutlineInputBorder( - borderSide: BorderSide(color: Colors.green), - ), - enabledBorder: InputBorder.none, - focusedBorder: const OutlineInputBorder( - borderSide: BorderSide(color: Colors.green), - ), - ), - ), - ], - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - style: TextButton.styleFrom( - foregroundColor: AppColors.green, - ), - child: Text('ok'.tr), - ), - TextButton( - onPressed: () { - Navigator.pop(context); - customLocationTextController.clear(); - setState(() {}); - }, - style: TextButton.styleFrom( - foregroundColor: Colors.red, - ), - child: Text('reset'.tr), - ) - ], - ), - ); - }, - title: Text( - 'setCustomLocation'.tr, - style: TextStyle( - fontSize: MediaQuery.of(context).size.height * 0.025, + TextButton( + onPressed: () { + Navigator.pop(context); + customLocationTextController.clear(); + setState(() {}); + }, + style: TextButton.styleFrom( + foregroundColor: Colors.red, ), - ), - ) - ], + child: Text('reset'.tr), + ) + ], + ), + ); + }, + title: Text( + 'setCustomLocation'.tr, + style: TextStyle( + fontSize: MediaQuery.of(context).size.height * 0.020, ), ), - ], - ), + ), + + // Subtitles + Expanded( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: TextField( + cursorColor: Colors.green, + maxLines: null, + onChanged: (value) => setState(() { + _subtitles = value; + }), + decoration: InputDecoration( + hintText: 'enterSubtitles'.tr, + filled: true, + border: const OutlineInputBorder( + borderSide: BorderSide(color: Colors.green), + ), + enabledBorder: InputBorder.none, + focusedBorder: const OutlineInputBorder( + borderSide: BorderSide(color: Colors.green), + ), + ), + ), + ), + ), + ], ); } } diff --git a/lib/pages/save_video/widgets/save_button.dart b/lib/pages/save_video/widgets/save_button.dart index 47450fa..30be3c2 100644 --- a/lib/pages/save_video/widgets/save_button.dart +++ b/lib/pages/save_video/widgets/save_button.dart @@ -25,6 +25,8 @@ class SaveButton extends StatefulWidget { required this.dateFormat, required this.isTextDate, required this.userLocation, + required this.subtitles, + required this.videoDuration, required this.isGeotaggingEnabled, required this.textOutlineColor, required this.textOutlineWidth, @@ -37,6 +39,8 @@ class SaveButton extends StatefulWidget { final String dateFormat; final bool isTextDate; final String? userLocation; + final String? subtitles; + final int videoDuration; final bool isGeotaggingEnabled; final Color textOutlineColor; final double textOutlineWidth; @@ -97,42 +101,30 @@ class _SaveButtonState extends State { Widget build(BuildContext context) { bool _pressedSave = false; - return SizedBox( - width: MediaQuery.of(context).size.width * 0.45, - height: MediaQuery.of(context).size.height * 0.08, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - elevation: 5.0, - backgroundColor: Colors.green, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(80.0), - ), - ), - child: !isProcessing - ? Text( - 'save'.tr, - style: TextStyle( - color: Colors.white, - fontSize: MediaQuery.of(context).size.width * 0.07, - ), - ) - : const CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation( - Colors.white, - ), + return FloatingActionButton( + backgroundColor: Colors.green, + child: !isProcessing + ? const Icon( + Icons.save, + color: Colors.white, + ) + : const CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation( + Colors.white, ), - onPressed: () { - // Prevents user from clicking it twice - if (!_pressedSave) { - _pressedSave = true; - _saveVideo(); - } - }, - ), + ), + onPressed: () { + // Prevents user from clicking it twice + if (!_pressedSave) { + _pressedSave = true; + _saveVideo(); + } + }, ); } - Future _editWithFFmpeg(bool isGeotaggingEnabled, BuildContext context) async { + Future _editWithFFmpeg( + bool isGeotaggingEnabled, BuildContext context) async { // Positions to render texts for the (x, y co-ordinates) // According to the ffmpeg docs, the x, y positions are relative to the top-left side of the output frame. final String datePosY = widget.isTextDate ? 'h-th-40' : '40'; @@ -144,6 +136,7 @@ class _SaveButtonState extends State { const double locTextSize = 33; String locOutput = ''; + String subtitles = ''; // Used to not increment videoCount controller bool isEdit = false; @@ -169,7 +162,8 @@ class _SaveButtonState extends State { } // Checks to ensure special read/write permissions with storage access framework - final hasSafDirPerms = await Saf.isPersistedPermissionDirectoryFor(finalPath) ?? false; + final hasSafDirPerms = + await Saf.isPersistedPermissionDirectoryFor(finalPath) ?? false; if (hasSafDirPerms) { await Saf(finalPath).getDirectoryPermission(isDynamic: true); } @@ -180,12 +174,21 @@ class _SaveButtonState extends State { ', drawtext=$fontPath:text=\'${widget.userLocation}\':fontsize=$locTextSize:fontcolor=\'$parsedDateColor\':borderw=${widget.textOutlineWidth}:bordercolor=$parsedTextOutlineColor:x=$locPosX:y=$locPosY'; } + // If subtitles TextBox were not left empty, we can allow the command to render the subtitles into the video + if (widget.subtitles != null && widget.subtitles != '') { + final subtitlesPath = await Utils.writeSrt( + widget.subtitles!, + widget.videoDuration, + ); + subtitles = '-i $subtitlesPath -c copy -c:s mov_text'; + } + // Caches the default font to save texts in ffmpeg. // The edit may fail unexpectedly in some devices if this is not done. await FFmpegKitConfig.setFontDirectory(fontPath); await executeFFmpeg( - '-i $videoPath -vf [in]drawtext="$fontPath:text=\'${widget.dateFormat}\':fontsize=$dateTextSize:fontcolor=\'$parsedDateColor\':borderw=${widget.textOutlineWidth}:bordercolor=$parsedTextOutlineColor:x=$datePosX:y=$datePosY$locOutput[out]" -codec:v libx264 -pix_fmt yuv420p $finalPath -y', + '-i $videoPath $subtitles -vf [in]drawtext="$fontPath:text=\'${widget.dateFormat}\':fontsize=$dateTextSize:fontcolor=\'$parsedDateColor\':borderw=${widget.textOutlineWidth}:bordercolor=$parsedTextOutlineColor:x=$datePosX:y=$datePosY$locOutput[out]" -codec:v libx264 -pix_fmt yuv420p $finalPath -y', ).then((session) async { print(session.getCommand().toString()); final returnCode = await session.getReturnCode(); @@ -215,7 +218,8 @@ class _SaveButtonState extends State { } else if (ReturnCode.isCancel(returnCode)) { print('Execution was cancelled'); } else { - print('Error editing video: Return code is ${await session.getReturnCode()}'); + print( + 'Error editing video: Return code is ${await session.getReturnCode()}'); final sessionLog = await session.getAllLogsAsString(); final failureStackTrace = await session.getFailStackTrace(); debugPrint('Session lasted for ${await session.getDuration()} ms'); diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index e88c38f..3594ef8 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -65,14 +65,16 @@ class Utils { if (androidDeviceInfo.version.sdkInt <= 32) { // For android 12 and below devices - permissionStatuses = - await [Permission.storage, Permission.manageExternalStorage].request(); + permissionStatuses = await [ + Permission.storage, + Permission.manageExternalStorage + ].request(); } else { permissionStatuses = await [ Permission.videos, Permission.photos, Permission.audio, - Permission.manageExternalStorage, + Permission.manageExternalStorage ].request(); } @@ -116,6 +118,47 @@ class Utils { return txtPath; } + /// Write srt file used by ffmpeg to add subtitles to the movie + static Future writeSrt(String text, int videoDuration) async { + final io.Directory directory = await getApplicationDocumentsDirectory(); + final String srtPath = '${directory.path}/subtitles.srt'; + + // Delete old srt files + if (StorageUtils.checkFileExists(srtPath)) StorageUtils.deleteFile(srtPath); + + final io.File file = io.File(srtPath); + + // Add linebreaks if a line is > 45 chars + text = '$text\n'; + final List lines = text.split('\n'); + text = ''; + for (int i = 0; i < lines.length; i++) { + if (lines[i].length > 45) { + final List words = lines[i].split(' '); + String temp = ''; + for (int j = 0; j < words.length; j++) { + if (temp.length + words[j].length > 45) { + text += '$temp\n'; + temp = ''; + } + temp += '${words[j]} '; + } + text += '$temp\n'; + } else { + text += '${lines[i]}\n'; + } + } + + final String totalSeconds = videoDuration == 10 ? '10' : '0$videoDuration'; + final String subtitles = + '1\r\n00:00:00,000 --> 00:00:$totalSeconds,000\r\n$text\r\n'; + + // Writing file + await file.writeAsString(subtitles, mode: io.FileMode.write); + + return srtPath; + } + /// Get all video files inside OneSecondDiary folder static List getAllVideos() { final directory = io.Directory(SharedPrefsUtil.getString('appPath')); @@ -172,7 +215,8 @@ class Utils { /// Get a filtered list of mp4 files names ordered by date to be written on a txt file /// To get all videos, use `ExportDateRange.allTime` - static List getSelectedVideosFromStorage(ExportDateRange exportDateRange) { + static List getSelectedVideosFromStorage( + ExportDateRange exportDateRange) { final now = DateTime.now(); final List allVideos = []; @@ -218,7 +262,9 @@ class Utils { for (int i = 0; i < allDates.length; i++) { // Retains all the dates from the beginning of the month until the current date allDates.retainWhere( - (e) => e.compareTo(DateTime(now.year, now.month)) >= 0 && e.compareTo(now) <= 0, + (e) => + e.compareTo(DateTime(now.year, now.month)) >= 0 && + e.compareTo(now) <= 0, ); } break; @@ -226,7 +272,8 @@ class Utils { for (int i = 0; i < allDates.length; i++) { // Retains all the dates from the start of the year until the current date within the year allDates.retainWhere( - (e) => e.compareTo(DateTime(now.year)) >= 0 && e.compareTo(now) <= 0, + (e) => + e.compareTo(DateTime(now.year)) >= 0 && e.compareTo(now) <= 0, ); } break; @@ -255,8 +302,9 @@ class Utils { // Converting back to string for (int i = 0; i < orderedDates.length; i++) { // Adding a leading zero on Days and Months <= 9 - final String day = - orderedDates[i].day <= 9 ? '0${orderedDates[i].day}' : '${orderedDates[i].day}'; + final String day = orderedDates[i].day <= 9 + ? '0${orderedDates[i].day}' + : '${orderedDates[i].day}'; final String month = orderedDates[i].month <= 9 ? '0${orderedDates[i].month}' : '${orderedDates[i].month}'; @@ -276,7 +324,8 @@ class Utils { // Utils().logInfo('Font already exists'); print('Font already exists'); } else { - final ByteData data = await rootBundle.load('assets/fonts/YuseiMagic-Regular.ttf'); + final ByteData data = + await rootBundle.load('assets/fonts/YuseiMagic-Regular.ttf'); final List bytes = data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes); await io.File(fontPath).writeAsBytes(bytes);