diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index dc445d1..40c5a30 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -8,6 +8,7 @@ - Harry Schiller (@waitingwittykitty) - David Coker (@daoxve) - Adrasteon (@AdrasteonDev) +- Philéas (@phileas_imt) ## Testing & Feedback - Augusto Vesco diff --git a/lib/lang/de.dart b/lib/lang/de.dart index 1cbd3b0..7af869e 100644 --- a/lib/lang/de.dart +++ b/lib/lang/de.dart @@ -110,6 +110,9 @@ const Map de = { 'profileNameCannotBeEmpty': 'Profilname kann nicht leer sein', 'reservedProfileName': 'Dies ist ein reservierter Profilname', 'creatingMovie': 'Verarbeitung... Bitte warten.\nDies kann mehrere Minuten dauern.', + 'verticalProfileName': 'Vertikales Profil', + 'verticalProfileActivated': 'Dieses Profil enthält nur Snippets im Hochformat.', + 'verticalProfileDisabled': 'Dieses Profil enthält nur Snippets im Querformat.', 'doNotCloseTheApp': 'Bitte schließen Sie die\nApp nicht', 'cancelMovieCreation': 'Film erstellen abbrechen', 'cancelMovieDesc': 'Möchtest Du wirklich abbrechen?', diff --git a/lib/lang/en.dart b/lib/lang/en.dart index 47ed045..69a9f1c 100644 --- a/lib/lang/en.dart +++ b/lib/lang/en.dart @@ -111,6 +111,9 @@ const Map en = { 'All videos associated with this profile will also be permanently deleted. Are you sure to continue?', 'profileNameCannotBeEmpty': 'Profile name cannot be empty', 'reservedProfileName': 'This is a reserved profile name', + 'verticalProfileName': 'Vertical Profile', + 'verticalProfileActivated': 'This profile will contain portrait snippets only.', + 'verticalProfileDisabled': 'This profile will contain landscape snippets only.', 'creatingMovie': 'Processing... Please wait.\nThis can take several minutes.', 'doNotCloseTheApp': 'Do not close the app', 'cancelMovieCreation': 'Cancel movie creation', @@ -209,4 +212,9 @@ const Map en = { 'useAlternativeCalendarColors': 'Use alternative calendar colors', 'useAlternativeCalendarColorsDescription': 'Changes green and red in calendar to blue and yellow. Useful for colorblind people.', + 'mixedResolutionAlert': 'Mixed Resolutions detected', + 'mixedResolutionAlertDescription': + 'At least one snippet seems to have a different resolution.\n\n' + 'This can cause unexpected results when using your film.\n\n' + 'Try to delete theses files or move them to a different profile.' }; diff --git a/lib/lang/es.dart b/lib/lang/es.dart index 6750222..d101d1e 100644 --- a/lib/lang/es.dart +++ b/lib/lang/es.dart @@ -110,6 +110,9 @@ const Map es = { 'Todos los videos asociados con este perfil también se eliminarán permanentemente.¿Estás seguro de continuar?', 'profileNameCannotBeEmpty': 'El nombre del perfil no puede estar vacío', 'reservedProfileName': 'Este es un nombre de perfil reservado', + 'verticalProfileName': 'Perfil vertical', + 'verticalProfileActivated': 'Este perfil sólo contendrá fragmentos verticales', + 'verticalProfileDisabled': 'Este perfil sólo contendrá fragmentos apaisados', 'creatingMovie': 'Procesando... Por favor espera.\nEsto puede tomar varios minutos.', 'doNotCloseTheApp': 'No cierres la aplicación', 'cancelMovieCreation': 'Cancelar creación de película', diff --git a/lib/lang/fr.dart b/lib/lang/fr.dart index b51ef88..c7f0058 100644 --- a/lib/lang/fr.dart +++ b/lib/lang/fr.dart @@ -111,6 +111,9 @@ const Map fr = { 'Toutes les vidéos associées à ce profil seront également supprimées en permanence. Êtes-vous sûr de continuer?', 'profileNameCannotBeEmpty': 'Le nom du profil ne peut pas être vide', 'reservedProfileName': 'Ceci est un nom de profil réservé', + 'verticalProfileName': 'Profil Vertical', + 'verticalProfileActivated': 'Ce profil contiendra uniquement des vidéos au format portrait.', + 'verticalProfileDisabled': 'Ce profil contiendra uniquement des vidéos au format paysage', 'creatingMovie': 'Traitement... Veuillez patienter.\nCela peut prendre quelques minutes.', 'doNotCloseTheApp': 'Ne fermez pas l\'application', 'cancelMovieCreation': 'Annuler la création du film', @@ -210,5 +213,9 @@ const Map fr = { 'Lorsqu\'il est activé, sélectionner des dates passées filtrera les vidéos par cette date. Lorsqu\'il est désactivé, toutes les vidéos seront affichées. Fonctionne uniquement avec le sélecteur de fichiers expérimental.', 'useAlternativeCalendarColors': 'Utilisez des couleurs de calendrier alternatives', 'useAlternativeCalendarColorsDescription': - 'Change le vert et le rouge dans le calendrier en bleu et jaune. Utile pour les personnes daltoniennes.' + 'Change le vert et le rouge dans le calendrier en bleu et jaune. Utile pour les personnes daltoniennes.', + 'mixedResolutionAlert': 'Mix de résolutions détecté.', + 'mixedResolutionAlertDescription': 'Au moins une vidéo semble avoir un mix de résolutions.\n\n' + 'Cela peut produire des résultats innatendus lorsque vous utilisez votre film.\n\n' + 'Essayez de supprimer ces fichiers ou de les déplacer dans un autre profil.' }; diff --git a/lib/models/profile.dart b/lib/models/profile.dart index a317565..809134a 100644 --- a/lib/models/profile.dart +++ b/lib/models/profile.dart @@ -1,12 +1,13 @@ -import 'package:flutter/material.dart'; - -@immutable class Profile { const Profile({ required this.label, + required this.storageString, this.isDefault = false, + this.isVertical = false, }); final String label; + final String storageString; final bool isDefault; + final bool isVertical; } diff --git a/lib/pages/home/base/home_page.dart b/lib/pages/home/base/home_page.dart index 5d2a558..b43fc91 100644 --- a/lib/pages/home/base/home_page.dart +++ b/lib/pages/home/base/home_page.dart @@ -19,9 +19,7 @@ class HomePage extends GetView { body: SizedBox( width: MediaQuery.of(context).size.width, height: MediaQuery.of(context).size.height, - child: Center( - child: Obx(() => _getSelectedPage(controller.activeIndex.value)), - ), + child: Obx(() => _getSelectedPage(controller.activeIndex.value)), ), ); } diff --git a/lib/pages/home/base/widgets/bottom_app_bar.dart b/lib/pages/home/base/widgets/bottom_app_bar.dart index 68f31e4..91d4039 100644 --- a/lib/pages/home/base/widgets/bottom_app_bar.dart +++ b/lib/pages/home/base/widgets/bottom_app_bar.dart @@ -4,7 +4,6 @@ import 'package:salomon_bottom_bar/salomon_bottom_bar.dart'; import '../../../../controllers/bottom_app_bar_index_controller.dart'; import '../../../../utils/constants.dart'; -import '../../../../utils/theme.dart'; SalomonBottomBarItem _bottomBarItem({ required IconData icon, @@ -28,7 +27,7 @@ class CustomBottomAppBar extends GetView { Widget build(BuildContext context) { return Obx( () => SalomonBottomBar( - backgroundColor: ThemeService().isDarkTheme() ? AppColors.dark : AppColors.light, + backgroundColor: Colors.black12.withOpacity(0.05), currentIndex: controller.activeIndex.value, onTap: controller.setBottomAppBarIndex, items: [ diff --git a/lib/pages/home/calendar_editor/calendar_editor_page.dart b/lib/pages/home/calendar_editor/calendar_editor_page.dart index 45f30c3..e27ccf1 100644 --- a/lib/pages/home/calendar_editor/calendar_editor_page.dart +++ b/lib/pages/home/calendar_editor/calendar_editor_page.dart @@ -79,7 +79,7 @@ class _CalendarEditorPageState extends State { } void setMediaStorePath() { - final currentProfile = Utils.getCurrentProfile(); + final currentProfile = Utils.getCurrentProfile().storageString; if (currentProfile.isEmpty || currentProfile == 'Default') { MediaStore.appFolder = 'OneSecondDiary'; } else { @@ -166,7 +166,7 @@ class _CalendarEditorPageState extends State { } bool shouldIgnoreExperimentalFilter() { - final useFilter = SharedPrefsUtil.getBool('useFilterInExperimentalPicker') ?? false; + final useFilter = SharedPrefsUtil.getBool('useFilterInExperimentalPicker') ?? true; if (!useFilter) return true; if (_selectedDate.day == DateTime.now().day && _selectedDate.month == DateTime.now().month && @@ -436,78 +436,78 @@ class _CalendarEditorPageState extends State { padding: const EdgeInsets.symmetric(horizontal: 20.0), child: AspectRatio( aspectRatio: 16 / 9, - child: Container( - decoration: BoxDecoration( - border: Border.all(color: mainColor), - ), - child: Stack( - children: [ - Center( - child: SizedBox( - height: 30, - width: 30, - child: Icon( - Icons.hourglass_bottom, - color: mainColor, - ), + child: Stack( + children: [ + Center( + child: SizedBox( + height: 30, + width: 30, + child: Icon( + Icons.hourglass_bottom, + color: mainColor, ), ), - FutureBuilder( - future: initializeVideoPlayback(currentVideo), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const SizedBox.shrink(); - } - - if (snapshot.hasError) { - return Text( - '"Error loading video: " + ${snapshot.error}', - ); - } - - // Not sure if it works but if the videoController fails we try to restart the page - if (_controller?.value.hasError == true) { - WidgetsBinding.instance.addPostFrameCallback((_) { - _controller?.dispose(); - }); - Get.offAllNamed(Routes.HOME) - ?.then((_) => setState(() {})); - } - - // VideoPlayer - if (_controller != null && - _controller!.value.isInitialized) { - return Align( - alignment: Alignment.center, - child: Stack( - fit: StackFit.passthrough, - children: [ - Align( - alignment: Alignment.center, - child: ClipRect( + ), + FutureBuilder( + future: initializeVideoPlayback(currentVideo), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const SizedBox.shrink(); + } + + if (snapshot.hasError) { + return Text( + '"Error loading video: " + ${snapshot.error}', + ); + } + + // Not sure if it works but if the videoController fails we try to restart the page + if (_controller?.value.hasError == true) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _controller?.dispose(); + }); + Get.offAllNamed(Routes.HOME)?.then((_) => setState(() {})); + } + + // VideoPlayer + if (_controller != null && _controller!.value.isInitialized) { + return Align( + alignment: Alignment.center, + child: Stack( + fit: StackFit.passthrough, + children: [ + Align( + alignment: Alignment.center, + child: Container( + decoration: BoxDecoration( + border: Border.all(color: mainColor), + ), + child: AspectRatio( + aspectRatio: _controller!.value.aspectRatio, child: VideoPlayer( key: _videoPlayerKey, _controller!, ), ), ), - Controls( - controller: _controller, - ), - ], - ), - ); - } else { - return const SizedBox.shrink(); - } - }, - ), - ], - ), + ), + Controls( + controller: _controller, + ), + ], + ), + ); + } else { + return const SizedBox.shrink(); + } + }, + ), + ], ), ), ), - Flexible( + Padding( + padding: const EdgeInsets.only(top: 15), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ diff --git a/lib/pages/home/calendar_editor/video_subtitles_editor_page.dart b/lib/pages/home/calendar_editor/video_subtitles_editor_page.dart index 474d944..ee6cfc6 100644 --- a/lib/pages/home/calendar_editor/video_subtitles_editor_page.dart +++ b/lib/pages/home/calendar_editor/video_subtitles_editor_page.dart @@ -170,35 +170,39 @@ class _VideoSubtitlesEditorPageState extends State { children: [ GestureDetector( onTap: () => videoPlay(), - child: AspectRatio( - aspectRatio: 16 / 9, - child: Stack( - children: [ - VideoPlayer( - key: UniqueKey(), - _videoController, - ), - Center( - child: Opacity( - opacity: _opacity, - child: Container( - width: MediaQuery.of(context).size.width * 0.25, - height: MediaQuery.of(context).size.width * 0.25, - decoration: const BoxDecoration( - color: Colors.black45, - shape: BoxShape.circle, - ), - child: const Center( - child: Icon( - Icons.play_arrow, - size: 72.0, - color: Colors.white, + // ConstrainedBox to fit vertical videos without overflowing + child: ConstrainedBox( + constraints: BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.7), + child: AspectRatio( + aspectRatio: _videoController.value.aspectRatio, + child: Stack( + children: [ + VideoPlayer( + key: UniqueKey(), + _videoController, + ), + Center( + child: Opacity( + opacity: _opacity, + child: Container( + width: MediaQuery.of(context).size.width * 0.25, + height: MediaQuery.of(context).size.width * 0.25, + decoration: const BoxDecoration( + color: Colors.black45, + shape: BoxShape.circle, + ), + child: const Center( + child: Icon( + Icons.play_arrow, + size: 72.0, + color: Colors.white, + ), ), ), ), ), - ), - ], + ], + ), ), ), ), @@ -218,7 +222,7 @@ class _VideoSubtitlesEditorPageState extends State { hintText: 'enterSubtitles'.tr.split('(').first, fillColor: ThemeService().isDarkTheme() ? Colors.black : Colors.white, hintStyle: TextStyle( - color: ThemeService().isDarkTheme() ? Colors.black : Colors.white, + color: ThemeService().isDarkTheme() ? Colors.white : Colors.black, ), filled: true, border: const OutlineInputBorder( diff --git a/lib/pages/home/create_movie/widgets/create_movie_button.dart b/lib/pages/home/create_movie/widgets/create_movie_button.dart index 831c48b..adfac09 100644 --- a/lib/pages/home/create_movie/widgets/create_movie_button.dart +++ b/lib/pages/home/create_movie/widgets/create_movie_button.dart @@ -39,6 +39,8 @@ class _CreateMovieButtonState extends State { final VideoCountController controller = Get.find(); bool isProcessing = false; String progress = ''; + String usedResolution = ''; + var mixedResolutionError = []; void _openVideo(String filePath) async { await OpenFilex.open(filePath); @@ -106,7 +108,7 @@ class _CreateMovieButtonState extends State { ScaffoldMessenger.of(context).showSnackBar(snackBar); // Get current profile - final currentProfileName = Utils.getCurrentProfile(); + final currentProfileName = Utils.getCurrentProfile().storageString; // Videos folder String videosFolder = SharedPrefsUtil.getString('appPath'); @@ -153,6 +155,27 @@ class _CreateMovieButtonState extends State { } }); + // Checks if there is a mix of horizontal/vertical videos by comparing their resolution. + await executeFFprobe( + '-v quiet -show_entries stream=width,height -of default=nw=1:nk=1 "$currentVideo"') + .then((session) async { + final returnCode = await session.getReturnCode(); + if (ReturnCode.isSuccess(returnCode)) { + final sessionLog = await session.getOutput(); + if (sessionLog == usedResolution) { + return; + } else if (usedResolution == '' && sessionLog!.isNotEmpty) { + usedResolution = sessionLog; + } else { + mixedResolutionError.add(currentVideo); + } + } else { + final sessionLog = await session.getLogsAsString(); + Utils.logError('${logTag}Error checking if $currentVideo was recorded on v1.5'); + Utils.logError('${logTag}Error: $sessionLog'); + } + }); + // Make sure all selected videos have a subtitles and audio stream before creating movie, and finally check their resolution, resizes if necessary. // To avoid asking permission for every single video, we make a copy and leave the original untouched if (!isV1point5) { @@ -278,6 +301,22 @@ class _CreateMovieButtonState extends State { }); } + // Show an error if multiple resolutions is detected. + if(mixedResolutionError.isNotEmpty) { + showDialog( + barrierDismissible: false, + context: Get.context!, + builder: (context) => CustomDialog( + isDoubleAction: false, + title: 'mixedResolutionAlert'.tr, + content: "${'mixedResolutionAlertDescription'.tr}\n\n${mixedResolutionError.toString()}", + actionText: 'Ok', + actionColor: Colors.red, + action: () => Get.offAllNamed(Routes.HOME), + ), + ); + } + if (mounted) { setState(() { progress = '$currentIndex / ${selectedVideos.length}'; diff --git a/lib/pages/home/create_movie/widgets/select_video_from_storage.dart b/lib/pages/home/create_movie/widgets/select_video_from_storage.dart index 5684730..6509309 100644 --- a/lib/pages/home/create_movie/widgets/select_video_from_storage.dart +++ b/lib/pages/home/create_movie/widgets/select_video_from_storage.dart @@ -43,6 +43,7 @@ class _SelectVideoFromStorageState extends State { Widget build(BuildContext context) { // Count all true in isSelected and return quantity final int totalSelected = isSelected?.where((element) => element).length ?? 0; + final aspectRatio = allVideos?.first.contains('_vertical') == true ? 0.5 : 1.0; return Scaffold( appBar: AppBar( iconTheme: const IconThemeData( @@ -115,9 +116,9 @@ class _SelectVideoFromStorageState extends State { cacheExtent: 99999, shrinkWrap: true, controller: scrollController, - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, - childAspectRatio: 1.12, + childAspectRatio: aspectRatio, ), itemCount: allVideos!.length, itemBuilder: (context, index) { diff --git a/lib/pages/home/create_movie/widgets/view_movies_page.dart b/lib/pages/home/create_movie/widgets/view_movies_page.dart index 501bce9..51343cb 100644 --- a/lib/pages/home/create_movie/widgets/view_movies_page.dart +++ b/lib/pages/home/create_movie/widgets/view_movies_page.dart @@ -102,6 +102,7 @@ class _ViewMoviesState extends State { alignment: Alignment.center, child: Image.memory( snapshot.data![index] as Uint8List, + height: MediaQuery.sizeOf(context).height * 0.27, ), ), Align( diff --git a/lib/pages/home/notification/widgets/switch_notifications.dart b/lib/pages/home/notification/widgets/switch_notifications.dart index 2d3be4e..e8f499c 100644 --- a/lib/pages/home/notification/widgets/switch_notifications.dart +++ b/lib/pages/home/notification/widgets/switch_notifications.dart @@ -8,12 +8,10 @@ import '../../../../utils/theme.dart'; class SwitchNotificationsComponent extends StatefulWidget { @override - _SwitchNotificationsComponentState createState() => - _SwitchNotificationsComponentState(); + _SwitchNotificationsComponentState createState() => _SwitchNotificationsComponentState(); } -class _SwitchNotificationsComponentState - extends State { +class _SwitchNotificationsComponentState extends State { late bool isNotificationSwitchToggled; TimeOfDay scheduledTimeOfDay = const TimeOfDay(hour: 20, minute: 00); late bool isPersistentSwitchToggled; @@ -58,10 +56,7 @@ class _SwitchNotificationsComponentState await notificationService.turnOnNotifications(); await notificationService.scheduleNotification( - scheduledTimeOfDay.hour, - scheduledTimeOfDay.minute, - DateTime.now() - ); + scheduledTimeOfDay.hour, scheduledTimeOfDay.minute, DateTime.now()); } else { await notificationService.turnOffNotifications(); } @@ -139,22 +134,17 @@ class _SwitchNotificationsComponentState }); } - notificationService.setScheduledTime(newTimeOfDay.hour, - newTimeOfDay.minute); + notificationService.setScheduledTime(newTimeOfDay.hour, newTimeOfDay.minute); setState(() { scheduledTimeOfDay = newTimeOfDay; }); await notificationService.scheduleNotification( - scheduledTimeOfDay.hour, - scheduledTimeOfDay.minute, - DateTime.now() - ); + scheduledTimeOfDay.hour, scheduledTimeOfDay.minute, DateTime.now()); }, child: Container( - padding: - const EdgeInsets.symmetric(horizontal: 15.0, vertical: 10.0), + padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 10.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -196,19 +186,16 @@ class _SwitchNotificationsComponentState } /// Schedule notification if switch in ON - if(isNotificationSwitchToggled && !isNotificationSwitchToggled){ + if (isNotificationSwitchToggled && !isNotificationSwitchToggled) { await notificationService.turnOnNotifications(); setState(() { isNotificationSwitchToggled = true; }); } - if(isNotificationSwitchToggled){ + if (isNotificationSwitchToggled) { await notificationService.scheduleNotification( - scheduledTimeOfDay.hour, - scheduledTimeOfDay.minute, - DateTime.now() - ); + scheduledTimeOfDay.hour, scheduledTimeOfDay.minute, DateTime.now()); } /// Update switch value @@ -222,7 +209,6 @@ class _SwitchNotificationsComponentState ], ), ), - const Divider(), ], ); } diff --git a/lib/pages/home/profiles/profiles_page.dart b/lib/pages/home/profiles/profiles_page.dart index c2f4fb9..b7a402d 100644 --- a/lib/pages/home/profiles/profiles_page.dart +++ b/lib/pages/home/profiles/profiles_page.dart @@ -31,6 +31,10 @@ class _ProfilesPageState extends State { final DailyEntryController dailyEntryController = Get.find(); + final List verticalModeSelector = [true, false]; + + bool _verticalModeSwitch = false; + @override void initState() { super.initState(); @@ -52,13 +56,20 @@ class _ProfilesPageState extends State { if (!storedProfiles.contains('Default')) { profiles.insert( 0, - const Profile(label: 'Default', isDefault: true), + const Profile( + label: 'Default', storageString: 'Default', isDefault: true, isVertical: false), ); } else { + // Profiles strings ending with '_vertical' creates an Profile object with isVertical value true, as other not. profiles = storedProfiles.map( (e) { - if (e == 'Default') return Profile(label: e, isDefault: true); - return Profile(label: e); + if (e == 'Default') + return Profile(label: e, storageString: e, isDefault: true, isVertical: false); + if (e.endsWith('_vertical')) + return Profile( + label: e.replaceAll('_vertical', ''), storageString: e, isVertical: true); + else + return Profile(label: e, storageString: e, isVertical: false); }, ).toList(); } @@ -87,7 +98,42 @@ class _ProfilesPageState extends State { Text( 'newProfileTooltip'.tr, ), - const SizedBox(height: 12), + const SizedBox(height: 24), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'verticalProfileName'.tr, + ), + Switch( + value: _verticalModeSwitch, + activeTrackColor: AppColors.mainColor.withOpacity(0.4), + activeColor: AppColors.mainColor, + onChanged: (value) { + setState(() { + _verticalModeSwitch = value; + final snackBar = SnackBar( + margin: const EdgeInsets.all(70.0), + behavior: SnackBarBehavior.floating, + backgroundColor: Colors.black54, + duration: const Duration(seconds: 3), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(25), + ), + ), + content: Text(_verticalModeSwitch + ? 'verticalProfileActivated'.tr + : 'verticalProfileDisabled'.tr), + ); + + ScaffoldMessenger.of(context).showSnackBar(snackBar); + }); + }, + ), + ], + ), + const SizedBox(height: 24), TextFormField( controller: _profileNameController, inputFormatters: [ @@ -143,13 +189,14 @@ class _ProfilesPageState extends State { actions: [ TextButton( onPressed: () async { - // Checks if the textfield is valid based on if the text passes all the validations we set + // Checks if the text field is valid based on if the text passes all the validations we set final bool isTextValid = _profileNameFormKey.currentState?.validate() ?? false; if (isTextValid) { // Create the profile directory for the new profile await StorageUtils.createSpecificProfileFolder( _profileNameController.text.trim(), + _verticalModeSwitch, ); Utils.logInfo( @@ -160,13 +207,20 @@ class _ProfilesPageState extends State { setState(() { profiles.insert( profiles.length, - Profile(label: _profileNameController.text.trim()), + Profile( + label: _profileNameController.text.trim(), + storageString: _verticalModeSwitch + ? '${_profileNameController.text.trim()}_vertical' + : _profileNameController.text.trim(), + isVertical: _verticalModeSwitch), ); _profileNameController.clear(); }); // Add the modified profile list to persistence - final profileNamesToStringList = profiles.map((e) => e.label).toList(); + // Adds the string '_vertical' at the end of vertical profiles to keep this parameter persistent. + final profileNamesToStringList = profiles.map((e) => e.storageString).toList(); + SharedPrefsUtil.putStringList('profiles', profileNamesToStringList); Navigator.pop(context); @@ -219,11 +273,11 @@ class _ProfilesPageState extends State { onPressed: () async { // Delete the profile directory for the specific profile await StorageUtils.deleteSpecificProfileFolder( - profiles[index].label, + profiles[index].storageString, ); Utils.logWarning( - '${logTag}Profile ${profiles[index].label} deleted!', + '${logTag}Profile ${profiles[index].storageString} deleted!', ); // Remove the profile from the list @@ -320,23 +374,36 @@ class _ProfilesPageState extends State { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), - title: Text( - profiles[index].isDefault ? 'default'.tr : profiles[index].label, - style: TextStyle( - color: ThemeService().isDarkTheme() ? Colors.white : Colors.black, - ), + title: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + profiles[index].isDefault ? 'default'.tr : profiles[index].label, + style: TextStyle( + color: ThemeService().isDarkTheme() ? Colors.white : Colors.black, + ), + ), + const SizedBox( + width: 5.0, + ), + RotatedBox( + quarterTurns: profiles[index].isVertical ? 0 : -1, + child: const Icon(Icons.phone_android), + ), + ], ), - secondary: profiles[index].isDefault - ? null - : IconButton( - onPressed: () async { - await _showDeleteProfileDialog(index); - }, - icon: const Icon( - Icons.delete_forever_rounded, - color: AppColors.mainColor, - ), + secondary: Row(mainAxisSize: MainAxisSize.min, children: [ + if (!profiles[index].isDefault) + IconButton( + onPressed: () async { + await _showDeleteProfileDialog(index); + }, + icon: const Icon( + Icons.delete_forever_rounded, + color: AppColors.mainColor, ), + ), + ]), ), ); }, @@ -370,7 +437,7 @@ class _ProfilesPageState extends State { // Update daily entry final String today = DateFormatUtils.getToday(); - final String profile = Utils.getCurrentProfile(); + final String profile = Utils.getCurrentProfile().label; String todaysVideoPath = SharedPrefsUtil.getString('appPath'); if (profile.isEmpty) { todaysVideoPath = '$todaysVideoPath$today.mp4'; diff --git a/lib/pages/home/settings/widgets/backup_tutorial.dart b/lib/pages/home/settings/widgets/backup_tutorial.dart index d4c8916..22009f3 100644 --- a/lib/pages/home/settings/widgets/backup_tutorial.dart +++ b/lib/pages/home/settings/widgets/backup_tutorial.dart @@ -31,7 +31,6 @@ class BackupTutorial extends StatelessWidget { ), ), ), - const Divider(), ], ); } diff --git a/lib/pages/home/settings/widgets/contact_button.dart b/lib/pages/home/settings/widgets/contact_button.dart index c07de6c..4bb40a5 100644 --- a/lib/pages/home/settings/widgets/contact_button.dart +++ b/lib/pages/home/settings/widgets/contact_button.dart @@ -40,7 +40,6 @@ class ContactButton extends StatelessWidget { ), ), ), - const Divider(), ], ); } diff --git a/lib/pages/home/settings/widgets/github_button.dart b/lib/pages/home/settings/widgets/github_button.dart index f05d11e..f7820d8 100644 --- a/lib/pages/home/settings/widgets/github_button.dart +++ b/lib/pages/home/settings/widgets/github_button.dart @@ -31,7 +31,6 @@ class GithubButton extends StatelessWidget { ), ), ), - const Divider(), ], ); } diff --git a/lib/pages/home/settings/widgets/language_chooser.dart b/lib/pages/home/settings/widgets/language_chooser.dart index d499e0e..c902a79 100644 --- a/lib/pages/home/settings/widgets/language_chooser.dart +++ b/lib/pages/home/settings/widgets/language_chooser.dart @@ -58,7 +58,6 @@ class _LanguageChooserState extends State { ], ), ), - const Divider(), ], ); } diff --git a/lib/pages/home/settings/widgets/notifications_button.dart b/lib/pages/home/settings/widgets/notifications_button.dart index fda32fb..36b81e9 100644 --- a/lib/pages/home/settings/widgets/notifications_button.dart +++ b/lib/pages/home/settings/widgets/notifications_button.dart @@ -13,8 +13,7 @@ class NotificationsButton extends StatelessWidget { InkWell( onTap: () => Get.toNamed(Routes.NOTIFICATION), child: Ink( - padding: - const EdgeInsets.symmetric(horizontal: 15.0, vertical: 10.0), + padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 10.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -29,7 +28,6 @@ class NotificationsButton extends StatelessWidget { ), ), ), - const Divider(), ], ); } diff --git a/lib/pages/home/settings/widgets/preferences_button.dart b/lib/pages/home/settings/widgets/preferences_button.dart index 9676c82..cebe3f2 100644 --- a/lib/pages/home/settings/widgets/preferences_button.dart +++ b/lib/pages/home/settings/widgets/preferences_button.dart @@ -22,8 +22,7 @@ class _PreferencesButtonState extends State { InkWell( onTap: () => Get.toNamed(Routes.PREFERENCES), child: Ink( - padding: const EdgeInsets.symmetric( - horizontal: 15.0, vertical: 10.0), + padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 10.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -38,7 +37,6 @@ class _PreferencesButtonState extends State { ), ), ), - const Divider(), ], ); } diff --git a/lib/pages/home/settings/widgets/preferences_page.dart b/lib/pages/home/settings/widgets/preferences_page.dart index cfb052a..a32080c 100644 --- a/lib/pages/home/settings/widgets/preferences_page.dart +++ b/lib/pages/home/settings/widgets/preferences_page.dart @@ -22,7 +22,7 @@ class _PreferencesPageState extends State { super.initState(); isCameraSwitchToggled = SharedPrefsUtil.getBool('forceNativeCamera') ?? false; isPickerSwitchToggled = SharedPrefsUtil.getBool('useExperimentalPicker') ?? true; - isPickerFilterSwitchToggled = SharedPrefsUtil.getBool('useFilterInExperimentalPicker') ?? false; + isPickerFilterSwitchToggled = SharedPrefsUtil.getBool('useFilterInExperimentalPicker') ?? true; isColorsSwitchToggled = SharedPrefsUtil.getBool('useAlternativeCalendarColors') ?? false; } diff --git a/lib/pages/home/settings/widgets/profiles_button.dart b/lib/pages/home/settings/widgets/profiles_button.dart index e6700eb..0164686 100644 --- a/lib/pages/home/settings/widgets/profiles_button.dart +++ b/lib/pages/home/settings/widgets/profiles_button.dart @@ -13,8 +13,7 @@ class ProfilesButton extends StatelessWidget { InkWell( onTap: () => Get.toNamed(Routes.PROFILES), child: Ink( - padding: - const EdgeInsets.symmetric(horizontal: 15.0, vertical: 10.0), + padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 10.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -29,7 +28,6 @@ class ProfilesButton extends StatelessWidget { ), ), ), - const Divider(), ], ); } diff --git a/lib/pages/home/settings/widgets/switch_theme.dart b/lib/pages/home/settings/widgets/switch_theme.dart index 8834fc6..f47d068 100644 --- a/lib/pages/home/settings/widgets/switch_theme.dart +++ b/lib/pages/home/settings/widgets/switch_theme.dart @@ -43,7 +43,6 @@ class _SwitchThemeComponentState extends State { ), ], ), - const Divider(), ], ); } diff --git a/lib/pages/save_video/save_video_page.dart b/lib/pages/save_video/save_video_page.dart index 64e697b..2c36757 100644 --- a/lib/pages/save_video/save_video_page.dart +++ b/lib/pages/save_video/save_video_page.dart @@ -10,6 +10,7 @@ import 'package:image_picker/image_picker.dart'; import 'package:video_trimmer/video_trimmer.dart'; import '../../controllers/recording_settings_controller.dart'; +import '../../models/profile.dart'; import '../../routes/app_pages.dart'; import '../../utils/constants.dart'; import '../../utils/custom_checkbox_list_tile.dart'; @@ -66,7 +67,7 @@ class _SaveVideoPageState extends State { bool _isLocationProcessing = false; late final bool isDarkTheme = ThemeService().isDarkTheme(); - String selectedProfileName = Utils.getCurrentProfile(); + Profile selectedProfile = Utils.getCurrentProfile(); void _initCorrectDates() { final DateTime selectedDate = routeArguments['currentDate']; @@ -533,6 +534,7 @@ class _SaveVideoPageState extends State { textOutlineWidth: textOutlineStrokeWidth, determinedDate: routeArguments['currentDate'], isFromRecordingPage: routeArguments['isFromRecordingPage'], + isVertical: selectedProfile.isVertical, ), ), body: Column( @@ -602,13 +604,22 @@ class _SaveVideoPageState extends State { const SizedBox(width: 8), Flexible( child: Text( - selectedProfileName.isEmpty ? 'default'.tr : selectedProfileName, + selectedProfile.label.isEmpty + ? 'default'.tr + : selectedProfile.isVertical + ? selectedProfile.label.replaceAll('_vertical', '') + : selectedProfile.label, style: TextStyle( fontSize: MediaQuery.of(context).size.height * 0.019, ), ), ), - const SizedBox(width: 20), + const SizedBox(width: 5), + RotatedBox( + quarterTurns: selectedProfile.isVertical ? 0 : -1, + child: const Icon(Icons.phone_android), + ), + const SizedBox(width: 15), Flexible( child: TextButton( style: ButtonStyle( @@ -623,7 +634,7 @@ class _SaveVideoPageState extends State { onPressed: () { Get.to(const ProfilesPage())?.then( (_) => setState(() { - selectedProfileName = Utils.getCurrentProfile(); + selectedProfile = Utils.getCurrentProfile(); }), ); }, @@ -982,8 +993,8 @@ class _SaveVideoPageState extends State { autofocus: true, controller: customLocationTextController, textCapitalization: TextCapitalization.sentences, - style: TextStyle( - color: ThemeService().isDarkTheme() ? Colors.black : Colors.white, + style: const TextStyle( + color: Colors.white, ), decoration: InputDecoration( hintText: 'enterLocation'.tr, diff --git a/lib/pages/save_video/widgets/save_button.dart b/lib/pages/save_video/widgets/save_button.dart index f5d1ca7..a61582e 100644 --- a/lib/pages/save_video/widgets/save_button.dart +++ b/lib/pages/save_video/widgets/save_button.dart @@ -34,6 +34,7 @@ class SaveButton extends StatefulWidget { required this.videoEndInMilliseconds, required this.determinedDate, required this.isFromRecordingPage, + required this.isVertical, }); // Finding controllers @@ -53,6 +54,7 @@ class SaveButton extends StatefulWidget { final double videoEndInMilliseconds; final DateTime determinedDate; final bool isFromRecordingPage; + final bool isVertical; @override _SaveButtonState createState() => _SaveButtonState(); @@ -315,9 +317,30 @@ class _SaveButtonState extends State { // Trim video to the selected range final trim = '-ss ${videoStartInMilliseconds}ms -to ${videoEndInMilliseconds}ms'; - // 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'; + // If video is created in a vertical profile, checks the aspect ratio. + String scale = ''; + if(widget.isVertical) { + + // Checks the aspect ratio of the video. + await executeFFprobe( + '-v error -select_streams v:0 -show_entries stream=display_aspect_ratio -of default=nw=1:nk=1 "$videoPath"') + .then((session) async { + final returnCode = await session.getReturnCode(); + if (ReturnCode.isSuccess(returnCode)) { + final sessionLog = await session.getOutput(); + // 4:3 videos (ex : produced by pixel devices in photo modes), will be scaled up to fit into 1080x1920. + if (sessionLog!.contains('4:3')) { + scale = 'scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920'; + Utils.logInfo('${logTag}4/3 video detected, cropping to 9/16.'); + } else { + scale = 'scale=1080:1920:force_original_aspect_ratio=decrease,pad=1080:1920:(ow-iw)/2:(oh-ih)/2:black'; + } + } + }); + // Scale video to 1920x1080 and add black padding if needed + } else { + 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 = diff --git a/lib/utils/storage_utils.dart b/lib/utils/storage_utils.dart index 9308d76..ead09f0 100644 --- a/lib/utils/storage_utils.dart +++ b/lib/utils/storage_utils.dart @@ -411,10 +411,16 @@ class StorageUtils { } // Create specific profile folder in internal storage - static Future createSpecificProfileFolder(String profileName) async { + static Future createSpecificProfileFolder(String profileName, bool verticalMode) async { try { final String appPath = SharedPrefsUtil.getString('appPath'); - final String profilePath = '$appPath/Profiles/$profileName/'; + + // Vertical Profiles are stored in a folder ending with _vertical. + if (verticalMode) { + profileName = '${profileName}_vertical'; + } + + final profilePath = '$appPath/Profiles/$profileName/'; // Checking if the folder really exists, if not, then create it final io.Directory? profileDirectory = io.Directory(profilePath); diff --git a/lib/utils/theme.dart b/lib/utils/theme.dart index 0e629fd..a77c772 100644 --- a/lib/utils/theme.dart +++ b/lib/utils/theme.dart @@ -36,6 +36,7 @@ class Themes { switchTheme: SwitchThemeData( thumbColor: MaterialStateProperty.all(AppColors.mainColor), trackColor: MaterialStateProperty.all(AppColors.rose), + trackOutlineColor: MaterialStateProperty.all(AppColors.mainColor), ), ); diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index 0bb05c2..467d2b7 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -12,6 +12,7 @@ import 'package:url_launcher/url_launcher.dart'; import '../controllers/video_count_controller.dart'; import '../enums/export_date_range.dart'; +import '../models/profile.dart'; import 'date_format_utils.dart'; import 'shared_preferences_util.dart'; import 'storage_utils.dart'; @@ -148,7 +149,7 @@ class Utils { logInfo('[Utils.writeTxt()] - Writing txt file to $txtPath'); // Get current profile - final currentProfileName = getCurrentProfile(); + final currentProfileName = getCurrentProfile().storageString; // Default directory String videosFolderPath = SharedPrefsUtil.getString('appPath'); @@ -252,23 +253,33 @@ class Utils { return '$hoursString:$minutesString:$secondsString,$millisecondsString'; } - /// Get current profile name, empty string if Default - static String getCurrentProfile() { + /// Get current profile object, empty string if Default. + /// As vertical profiles are saved with suffix '_vertical', + /// storageString = what's in storage, label = name without suffix. + static Profile getCurrentProfile() { // Get current profile - String currentProfileName = ''; + String currentProfileStorageString = ''; + String currentProfileLabel = ''; + bool isVertical = false; final selectedProfileIndex = SharedPrefsUtil.getInt('selectedProfileIndex') ?? 0; if (selectedProfileIndex != 0) { final allProfiles = SharedPrefsUtil.getStringList('profiles'); if (allProfiles != null) { - currentProfileName = allProfiles[selectedProfileIndex]; + currentProfileStorageString = allProfiles[selectedProfileIndex]; + + // Vertical profiles are stored with '_vertical' in storage, but shown without. + if(currentProfileStorageString.endsWith('_vertical')) { + isVertical = true; + currentProfileLabel = currentProfileStorageString.replaceAll('_vertical', ''); + } } } - final profileLog = currentProfileName == '' ? 'Default' : currentProfileName; + final profileLog = currentProfileStorageString == '' ? 'Default' : currentProfileStorageString; logInfo('[Utils.getCurrentProfile()] - Selected profile: $profileLog'); - return currentProfileName; + return Profile(storageString: currentProfileStorageString, label: currentProfileLabel, isVertical: isVertical); } /// Get all video files inside DCIM/OneSecondDiary/Movies folder @@ -301,7 +312,7 @@ class Utils { static List getAllVideos({bool fullPath = false}) { logInfo('[Utils.getAllVideos()] - Asked for full path: $fullPath'); // Get current profile - final currentProfileName = getCurrentProfile(); + final currentProfileName = getCurrentProfile().storageString; // Default directory io.Directory directory = io.Directory(SharedPrefsUtil.getString('appPath'));