diff --git a/README.md b/README.md index fafeec0d..2a33c277 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ The app is free, forever: no ads, no spam, no need to sign up or pay. Medito App is a flutter project available on Android and iOS maintained by the Medito Foundation and the community. + ## Download the app - Play Store: https://play.google.com/store/apps/details?id=meditofoundation.medito - App Store: https://apps.apple.com/us/app/medito/id1500780518 @@ -15,6 +16,13 @@ Medito App is a flutter project available on Android and iOS maintained by the M NOTE: If you install Medito app using APK file, please make sure to verify that the APK file is signed by Medito Foundation. See [VERIFY_APK](VERIFY_APK.md) for more information. +## Install +[![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/meditohq/medito-app?label=latest%20version&sort=semver)](https://github.com/meditohq/medito-app/releases) + +| Android | iOS | +| :--: | :--: | +| Get it on Google Play
|Download on the App Store | + ## How to use this code The best way to start is by opening the project with [Android Studio](https://developer.android.com/studio) or [Visual Studio](https://visualstudio.microsoft.com/) diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 186b7155..3c4101c3 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/ios/Flutter/flutter_export_environment.sh b/ios/Flutter/flutter_export_environment.sh index 3510029c..f7a1fa03 100755 --- a/ios/Flutter/flutter_export_environment.sh +++ b/ios/Flutter/flutter_export_environment.sh @@ -1,14 +1,14 @@ #!/bin/sh # This is a generated file; do not edit or check into version control. -export "FLUTTER_ROOT=/Users/mike/Development/flutter" -export "FLUTTER_APPLICATION_PATH=/Users/mike/Projects/medito-app" -export "FLUTTER_TARGET=/Users/mike/Projects/medito-app/lib/main.dart" +export "FLUTTER_ROOT=C:\src\flutter\flutter" +export "FLUTTER_APPLICATION_PATH=C:\Users\Harshad\Desktop\Workspace\medito-app" +export "COCOAPODS_PARALLEL_CODE_SIGN=true" +export "FLUTTER_TARGET=lib\main.dart" export "FLUTTER_BUILD_DIR=build" -export "SYMROOT=${SOURCE_ROOT}/../build/ios" -export "FLUTTER_BUILD_NAME=2.0.16" -export "FLUTTER_BUILD_NUMBER=20016" -export "DART_DEFINES=flutter.inspector.structuredErrors%3Dtrue" +export "SYMROOT=${SOURCE_ROOT}/../build\ios" +export "FLUTTER_BUILD_NAME=2.0.24" +export "FLUTTER_BUILD_NUMBER=20024" export "DART_OBFUSCATION=false" -export "TRACK_WIDGET_CREATION=true" +export "TRACK_WIDGET_CREATION=false" export "TREE_SHAKE_ICONS=false" -export "PACKAGE_CONFIG=/Users/mike/Projects/medito-app/.dart_tool/package_config.json" +export "PACKAGE_CONFIG=.packages" diff --git a/lib/audioplayer/audio_player_service.dart b/lib/audioplayer/audio_player_service.dart index 1dd6d9d5..3a2aa2b1 100644 --- a/lib/audioplayer/audio_player_service.dart +++ b/lib/audioplayer/audio_player_service.dart @@ -14,6 +14,7 @@ import 'package:just_audio/just_audio.dart'; const fadeDuration = 20; const PLAY_BG_SOUND = 'play_bg_sound'; const SEND_BG_SOUND = 'send_bg_sound'; +const INIT_BG_SOUND = 'init_bg_sound'; const SET_BG_SOUND_VOL = 'set_bg_sound_vol'; /// This task defines logic for playing a list of podcast episodes. @@ -24,11 +25,11 @@ class AudioPlayerTask extends BackgroundAudioTask { StreamSubscription _eventSubscription; var _duration = Duration(); - var _currentlyPlayingBGSound; + var _currentlyPlayingBGSound = ''; int get index => _player.currentIndex; MediaItem mediaItem; - var initialBgVolume = 0.6; + var initialBgVolume = 0.4; var _updatedStats = false; @override @@ -132,7 +133,9 @@ class AudioPlayerTask extends BackgroundAudioTask { @override Future onPlay() { - _bgPlayer.play(); + if(_currentlyPlayingBGSound.isNotEmptyAndNotNull()) { + _bgPlayer.play(); + } return _player.play(); } @@ -156,10 +159,12 @@ class AudioPlayerTask extends BackgroundAudioTask { await _player.stop(); await _broadcastState(); break; + case INIT_BG_SOUND: + AudioServiceBackground.sendCustomEvent( + {SEND_BG_SOUND: _currentlyPlayingBGSound}); + break; case SEND_BG_SOUND: - if ((params as String).isNotEmptyAndNotNull()) { - _currentlyPlayingBGSound = params; - } + _currentlyPlayingBGSound = params ?? ''; AudioServiceBackground.sendCustomEvent( {SEND_BG_SOUND: _currentlyPlayingBGSound}); break; diff --git a/lib/network/downloads/downloads_bloc.dart b/lib/network/downloads/downloads_bloc.dart index ab054460..ec6e726f 100644 --- a/lib/network/downloads/downloads_bloc.dart +++ b/lib/network/downloads/downloads_bloc.dart @@ -80,4 +80,13 @@ class DownloadsBloc { list.removeWhere((element) => MediaItem.fromJson(jsonDecode(element)).id == mediaFile.id); await prefs.setStringList(savedFilesKey, list); } + + // This method is used to save the updated order of the list of downloaded sessions + /// Saves a given `List` of `MediaItem` elements (downloaded sessions) + static Future saveDownloads(List mediaList) async { + var prefs = await SharedPreferences.getInstance(); + var list = + mediaList.map((MediaItem mediaItem) => jsonEncode(mediaItem)).toList(); + await prefs.setStringList(savedFilesKey, list); + } } diff --git a/lib/utils/strings.dart b/lib/utils/strings.dart index f0d45fa8..2db7e003 100644 --- a/lib/utils/strings.dart +++ b/lib/utils/strings.dart @@ -2,7 +2,8 @@ const String DOWNLOADS = 'Downloads'; const String DOWNLOAD = 'Download'; const String SOUNDS = 'Sounds'; const String BEGIN = 'Begin'; -const String PICK_NARRATOR = 'PICK A NARRATOR & DURATION'; +const String PICK_NARRATOR_AND_DURATION = 'Pick a narrator & duration'; +const String PICK_DURATION = 'Pick a duration'; const String SHOW_DOWNLOADS = 'Show downloads'; const String FAVOURITES = 'Favouites'; const String EMPTY_DOWNLOADS_MESSAGE = diff --git a/lib/widgets/btm_nav/downloads_widget.dart b/lib/widgets/btm_nav/downloads_widget.dart index ada38c10..bd12d86b 100644 --- a/lib/widgets/btm_nav/downloads_widget.dart +++ b/lib/widgets/btm_nav/downloads_widget.dart @@ -40,20 +40,29 @@ class _DownloadsListWidgetState extends State hasCloseButton: true, ), key: scaffoldKey, - body: _downloadList.isEmpty - ? _getEmptyWidget() - : _getDownloadList(), + body: _downloadList.isEmpty ? _getEmptyWidget() : _getDownloadList(), ); } Widget _getDownloadList() { - return ListView.builder( - padding: EdgeInsets.symmetric(vertical: 8), - itemCount: _downloadList.length, - itemBuilder: (context, i) { - var item = _downloadList[i]; - return _getSlidingItem(item, context); + // In order for the Dismissible action still to work on the list items, + // the default ReorderableListView is used (instead of the .builder one) + return ReorderableListView( + padding: EdgeInsets.symmetric(vertical: 8), + onReorder: (int oldIndex, int newIndex) { + setState(() { + if (oldIndex < newIndex) { + newIndex -= 1; + } + var reorderedItem = _downloadList.removeAt(oldIndex); + _downloadList.insert(newIndex, reorderedItem); + // To ensure, that the new list order is saved + DownloadsBloc.saveDownloads(_downloadList); }); + }, + children: + _downloadList.map((item) => _getSlidingItem(item, context)).toList(), + ); } Widget _getEmptyWidget() => EmptyStateWidget( @@ -67,6 +76,8 @@ class _DownloadsListWidgetState extends State Widget _getSlidingItem(MediaItem item, BuildContext context) { return InkWell( + // This (additional) key is required in order for the ReorderableListView to distinguish between the different list items + key: ValueKey(item.id), onTap: () { _openPlayer(item, context); }, diff --git a/lib/widgets/header_widget.dart b/lib/widgets/header_widget.dart index cb3390cb..c6e2ccf3 100644 --- a/lib/widgets/header_widget.dart +++ b/lib/widgets/header_widget.dart @@ -24,15 +24,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; class HeaderWidget extends StatelessWidget { - HeaderWidget({Key key, + HeaderWidget({ + Key key, this.primaryColorController, this.titleController, this.coverController, this.backgroundImageController, this.descriptionController, this.whiteText = false, - }) - : super(key: key); + }) : super(key: key); final StreamController primaryColorController; final StreamController titleController; final StreamController> coverController; @@ -66,15 +66,11 @@ class HeaderWidget extends StatelessWidget { children: [ MeditoAppBarWidget(transparent: true), _getRow(), - Container(height: whiteText ? 0 : 16) ], ), ], ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: _getDescriptionWidget(), - ) + _getDescriptionWidget(), ], ), ); @@ -123,21 +119,17 @@ class HeaderWidget extends StatelessWidget { ); } - StreamBuilder _getTitleStream() => - StreamBuilder( - initialData: '', - stream: titleController.stream, - builder: (context, snapshot) { - return Text( - snapshot.data, - maxLines: 3, - overflow: TextOverflow.ellipsis, - style: Theme - .of(context) - .textTheme - .headline1, - ); - }); + StreamBuilder _getTitleStream() => StreamBuilder( + initialData: '', + stream: titleController.stream, + builder: (context, snapshot) { + return Text( + snapshot.data, + maxLines: 3, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.headline1, + ); + }); StreamBuilder> _actualImageStream() { return StreamBuilder>( @@ -153,7 +145,7 @@ class HeaderWidget extends StatelessWidget { child: CircularProgressIndicator( backgroundColor: MeditoColors.transparent, valueColor: - AlwaysStoppedAnimation(MeditoColors.darkMoon), + AlwaysStoppedAnimation(MeditoColors.darkMoon), )), ); break; @@ -194,7 +186,8 @@ class HeaderWidget extends StatelessWidget { builder: (context, snapshot) { if (snapshot.hasData && snapshot.data.isNotEmptyAndNotNull()) { return Padding( - padding: const EdgeInsets.only(bottom: 20.0), + padding: + const EdgeInsets.symmetric(horizontal: 16.0, vertical: 20.0), child: Markdown( data: snapshot?.data ?? '', onTapLink: _linkTap, @@ -202,20 +195,16 @@ class HeaderWidget extends StatelessWidget { physics: NeverScrollableScrollPhysics(), styleSheet: MarkdownStyleSheet.fromTheme(Theme.of(context)) .copyWith( - p: Theme - .of(context) - .textTheme - .subtitle1 - .copyWith( - fontSize: 14.0, - color: whiteText - ? MeditoColors.walterWhite - : MeditoColors.meditoTextGrey)), + p: Theme.of(context).textTheme.subtitle1.copyWith( + fontSize: 14.0, + color: whiteText + ? MeditoColors.walterWhite + : MeditoColors.meditoTextGrey)), shrinkWrap: true, ), ); } else { - return Container(); + return Container(height: 20); } }); } diff --git a/lib/widgets/home/small_shortcuts_row_widget.dart b/lib/widgets/home/small_shortcuts_row_widget.dart index 15e9b415..e4513ad8 100644 --- a/lib/widgets/home/small_shortcuts_row_widget.dart +++ b/lib/widgets/home/small_shortcuts_row_widget.dart @@ -18,7 +18,7 @@ class SmallShortcutsRowWidget extends StatefulWidget { class SmallShortcutsRowWidgetState extends State { final _bloc = ShortcutsBloc(); - + bool isLandscape = false; @override void initState() { super.initState(); @@ -31,46 +31,52 @@ class SmallShortcutsRowWidgetState extends State { @override Widget build(BuildContext context) { - return SizeChangedLayoutNotifier( - child: StreamBuilder>( - stream: _bloc.shortcutList.stream, - initialData: ApiResponse.loading(), - builder: (context, snapshot) { - switch (snapshot.data.status) { - case Status.LOADING: - return _getLoadingWidget(); - break; - case Status.COMPLETED: - return GridView.count( - crossAxisCount: 2, - padding: - const EdgeInsets.only(left: 12.0, right: 12.0, top: 8.0), - scrollDirection: Axis.vertical, - childAspectRatio: 2.6, - physics: NeverScrollableScrollPhysics(), - shrinkWrap: true, - children: List.generate(snapshot.data.body?.data?.length ?? 0, - (index) { - return Card( - clipBehavior: Clip.antiAlias, - color: MeditoColors.deepNight, - child: SmallShortcutWidget( - snapshot.data.body.data[index], widget.onTap), + return OrientationBuilder(builder: (context, orientation) { + if (MediaQuery.of(context).size.width > 600) { + isLandscape = true; + } else { + isLandscape = false; + } + return SizeChangedLayoutNotifier( + child: StreamBuilder>( + stream: _bloc.shortcutList.stream, + initialData: ApiResponse.loading(), + builder: (context, snapshot) { + switch (snapshot.data.status) { + case Status.LOADING: + return _getLoadingWidget(); + break; + case Status.COMPLETED: + return GridView.count( + crossAxisCount: isLandscape ? 4 : 2, + padding: const EdgeInsets.only( + left: 12.0, right: 12.0, top: 8.0), + scrollDirection: Axis.vertical, + childAspectRatio: 2.6, + physics: NeverScrollableScrollPhysics(), + shrinkWrap: true, + children: List.generate( + snapshot.data.body?.data?.length ?? 0, (index) { + return Card( + clipBehavior: Clip.antiAlias, + color: MeditoColors.deepNight, + child: SmallShortcutWidget( + snapshot.data.body.data[index], widget.onTap), + ); + }), ); - }), - ); - break; - case Status.ERROR: - return Icon(Icons.error); - break; - } - return Container(); - }), - ); + break; + case Status.ERROR: + return Icon(Icons.error); + break; + } + return Container(); + })); + }); } Widget _getLoadingWidget() => GridView.count( - crossAxisCount: 2, + crossAxisCount: isLandscape ? 4 : 2, padding: const EdgeInsets.only(left: 12.0, right: 12.0, top: 8.0), scrollDirection: Axis.vertical, childAspectRatio: 2.6, diff --git a/lib/widgets/player/background_sounds_sheet_widget.dart b/lib/widgets/player/background_sounds_sheet_widget.dart index 032d9514..42a707f5 100644 --- a/lib/widgets/player/background_sounds_sheet_widget.dart +++ b/lib/widgets/player/background_sounds_sheet_widget.dart @@ -26,7 +26,6 @@ import 'package:Medito/widgets/player/position_indicator_widget.dart'; import 'package:audio_service/audio_service.dart'; import 'package:back_button_interceptor/back_button_interceptor.dart'; import 'package:flutter/material.dart'; -import 'package:pedantic/pedantic.dart'; import 'package:rxdart/rxdart.dart'; class ChooseBackgroundSoundDialog extends StatefulWidget { @@ -68,7 +67,7 @@ class _ChooseBackgroundSoundDialogState volume = await retrieveSavedBgVolume(); _dragBgVolumeSubject.add(volume); await AudioService.customAction(SET_BG_SOUND_VOL, volume / 100); - await AudioService.customAction(SEND_BG_SOUND); + await AudioService.customAction(INIT_BG_SOUND, ""); } } @@ -80,7 +79,7 @@ class _ChooseBackgroundSoundDialogState builder: (context, snapshot) { var currentSounds; try { - currentSounds = snapshot.data[SEND_BG_SOUND] ?? NONE; + currentSounds = (snapshot.data[SEND_BG_SOUND] as String).isNotEmptyAndNotNull() ? snapshot.data[SEND_BG_SOUND] : NONE; } catch (e) { currentSounds = NONE; } @@ -222,7 +221,7 @@ class _ChooseBackgroundSoundDialogState ); void _noneSelected() { - AudioService.customAction(SEND_BG_SOUND, NONE); + AudioService.customAction(SEND_BG_SOUND, ''); AudioService.customAction(PLAY_BG_SOUND, ''); addBgSoundSelectionToSharedPrefs('', ''); } diff --git a/lib/widgets/session_options/session_options_screen.dart b/lib/widgets/session_options/session_options_screen.dart index 355a0d1a..3cdd3785 100644 --- a/lib/widgets/session_options/session_options_screen.dart +++ b/lib/widgets/session_options/session_options_screen.dart @@ -13,6 +13,8 @@ Affero GNU General Public License for more details. You should have received a copy of the Affero GNU General Public License along with Medito App. If not, see .*/ +import 'dart:ui'; + import 'package:Medito/network/api_response.dart'; import 'package:Medito/network/downloads/downloads_bloc.dart'; import 'package:Medito/network/session_options/session_options_bloc.dart'; @@ -114,7 +116,8 @@ class _SessionOptionsScreenState extends State { style: Theme.of(context) .textTheme .subtitle2 - .copyWith(color: parseColor(snapshot.data))); + .copyWith( + color: parseColor(snapshot.data))); }), )), ) @@ -167,6 +170,9 @@ class _SessionOptionsScreenState extends State { Widget _buildOptionsPanel(List items) { var childList = []; + // To check whether any of the VoiceItems contains a narrator + var containsNarrator = + items.any((voiceItem) => voiceItem.headerValue.isNotEmptyAndNotNull()); items.forEach((value) { var section = _getListItem(context, value); @@ -186,11 +192,13 @@ class _SessionOptionsScreenState extends State { Container(height: 20), Padding( padding: EdgeInsets.only(left: 16), - child: Text(PICK_NARRATOR.toUpperCase(), - style: Theme.of(context) - .textTheme - .bodyText1 - .copyWith(color: MeditoColors.meditoTextGrey))), + child: Text( + containsNarrator + ? PICK_NARRATOR_AND_DURATION.toUpperCase() + : PICK_DURATION.toUpperCase(), + style: Theme.of(context).textTheme.bodyText1.copyWith( + color: MeditoColors.meditoTextGrey, + fontWeight: FontWeight.w600))), Container(height: 12), Column(children: childList), ]))); @@ -276,22 +284,21 @@ class _DownloadPanelWidgetState extends State { borderRadius: BorderRadius.circular(16), ), child: Padding( - padding: EdgeInsets.symmetric(vertical: 20, horizontal: 16), + padding: EdgeInsets.only(top: 20, bottom: 16, left: 16, right: 16), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(DOWNLOAD.toUpperCase(), - style: Theme.of(context).textTheme.bodyText2), + style: Theme.of(context).textTheme.bodyText1.copyWith( + color: MeditoColors.meditoTextGrey, + fontWeight: FontWeight.w600)), Container(height: 8), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(_getDownloadLabel(), - style: Theme.of(context) - .textTheme - .bodyText1 - .copyWith(color: MeditoColors.meditoTextGrey)), + style: Theme.of(context).textTheme.bodyText1), _getTrailing() ], ) diff --git a/pubspec.yaml b/pubspec.yaml index 90ed237d..18504bbd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: A meditation learning tool # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 2.0.23+20023 +version: 2.0.24+20024 environment: sdk: ">=2.6.0 <3.0.0" @@ -27,10 +27,10 @@ dependencies: shared_preferences: ^2.0.5 cached_network_image: ^3.0.0 auto_size_text: ^3.0.0-nullsafety.0 - just_audio: ^0.7.4 + just_audio: ^0.9.5 audio_session: ^0.1.0 audio_service: ^0.17.0 - rxdart: ^0.26.0 + rxdart: ^0.27.1 share: ^2.0.1 firebase_core: ^1.1.0 firebase_messaging: ^10.0.0