├── fastlane └── metadata │ └── android │ └── en-US │ ├── title.txt │ ├── changelogs │ ├── 4.txt │ ├── 8.txt │ ├── 6.txt │ ├── 9.txt │ ├── 17.txt │ ├── 7.txt │ ├── 5.txt │ └── 16.txt │ ├── short_description.txt │ ├── images │ ├── featureGraphic.png │ ├── icon.png │ └── phoneScreenshots │ │ ├── 1.png │ │ ├── 3.png │ │ ├── 4.png │ │ └── 2.png │ └── full_description.txt ├── ios ├── Runner │ ├── Runner-Bridging-Header.h │ ├── Assets.xcassets │ │ ├── LaunchImage.imageset │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ ├── README.md │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-50x50@1x.png │ │ │ ├── Icon-App-50x50@2x.png │ │ │ ├── Icon-App-57x57@1x.png │ │ │ ├── Icon-App-57x57@2x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-72x72@1x.png │ │ │ ├── Icon-App-72x72@2x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ └── Contents.json │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.storyboard │ └── Info.plist ├── Flutter │ ├── Debug.xcconfig │ ├── Release.xcconfig │ └── AppFrameworkInfo.plist ├── Runner.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ ├── xcshareddata │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ └── project.pbxproj ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── WorkspaceSettings.xcsettings │ │ └── IDEWorkspaceChecks.plist ├── .gitignore ├── Podfile └── Podfile.lock ├── media ├── logo-wide.png ├── feature-graphic.png ├── icon-enso-208px.png ├── screenshots │ ├── main.png │ ├── settings.png │ ├── custom-time.png │ ├── settings-2.png │ ├── time-selection.png │ └── old │ │ ├── screenshot-timer.png │ │ └── screenshot-settings.png └── icon-playstore-512px.png ├── assets ├── bell_burma.ogg ├── bell_indian.ogg ├── bell_singing.ogg ├── bowl_singing.ogg ├── gong_bodhi.ogg ├── gong_watts.ogg ├── gong_generated.ogg ├── bell_burma_three.ogg ├── bell_meditation.ogg ├── bowl_singing_big.ogg └── icon │ ├── icon-material-192px.png │ ├── icon-material-512px.png │ ├── icon-adaptive-background-432px.png │ └── icon-adaptive-foreground-432px.png ├── .gitmodules ├── android ├── app │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── drawable-hdpi │ │ │ │ │ ├── ic_notification.png │ │ │ │ │ ├── ic_launcher_foreground.png │ │ │ │ │ └── ic_launcher_monochrome.png │ │ │ │ ├── drawable-mdpi │ │ │ │ │ ├── ic_notification.png │ │ │ │ │ ├── ic_launcher_foreground.png │ │ │ │ │ └── ic_launcher_monochrome.png │ │ │ │ ├── drawable-xhdpi │ │ │ │ │ ├── ic_notification.png │ │ │ │ │ ├── ic_launcher_foreground.png │ │ │ │ │ └── ic_launcher_monochrome.png │ │ │ │ ├── drawable-xxhdpi │ │ │ │ │ ├── ic_notification.png │ │ │ │ │ ├── ic_launcher_foreground.png │ │ │ │ │ └── ic_launcher_monochrome.png │ │ │ │ ├── drawable-xxxhdpi │ │ │ │ │ ├── ic_notification.png │ │ │ │ │ ├── ic_launcher_foreground.png │ │ │ │ │ └── ic_launcher_monochrome.png │ │ │ │ ├── values │ │ │ │ │ ├── colors.xml │ │ │ │ │ └── styles.xml │ │ │ │ ├── drawable │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable-v21 │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ │ └── ic_launcher.xml │ │ │ │ └── values-night │ │ │ │ │ └── styles.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── nyxkn │ │ │ │ │ └── meditation │ │ │ │ │ └── MainActivity.kt │ │ │ └── AndroidManifest.xml │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── proguard-rules.pro │ └── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── .gitignore ├── build.gradle └── settings.gradle ├── .metadata ├── .gitignore ├── test └── widget_test.dart ├── analysis_options.yaml ├── lib ├── audioplayer.dart ├── main.dart ├── settings.dart ├── utils.dart └── timer.dart ├── pubspec.yaml ├── README.md └── pubspec.lock /fastlane/metadata/android/en-US/title.txt: -------------------------------------------------------------------------------- 1 | Meditation 2 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/4.txt: -------------------------------------------------------------------------------- 1 | * Released app onto F-Droid 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/short_description.txt: -------------------------------------------------------------------------------- 1 | An essential meditation timer 2 | -------------------------------------------------------------------------------- /media/logo-wide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/media/logo-wide.png -------------------------------------------------------------------------------- /assets/bell_burma.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/assets/bell_burma.ogg -------------------------------------------------------------------------------- /assets/bell_indian.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/assets/bell_indian.ogg -------------------------------------------------------------------------------- /assets/bell_singing.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/assets/bell_singing.ogg -------------------------------------------------------------------------------- /assets/bowl_singing.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/assets/bowl_singing.ogg -------------------------------------------------------------------------------- /assets/gong_bodhi.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/assets/gong_bodhi.ogg -------------------------------------------------------------------------------- /assets/gong_watts.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/assets/gong_watts.ogg -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/featureGraphic.png: -------------------------------------------------------------------------------- 1 | ../../../../../media/feature-graphic.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/icon.png: -------------------------------------------------------------------------------- 1 | ../../../../../assets/icon/icon-material-512px.png -------------------------------------------------------------------------------- /assets/gong_generated.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/assets/gong_generated.ogg -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/8.txt: -------------------------------------------------------------------------------- 1 | * Increased maximum meditation time to 5 hours 2 | -------------------------------------------------------------------------------- /media/feature-graphic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/media/feature-graphic.png -------------------------------------------------------------------------------- /media/icon-enso-208px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/media/icon-enso-208px.png -------------------------------------------------------------------------------- /assets/bell_burma_three.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/assets/bell_burma_three.ogg -------------------------------------------------------------------------------- /assets/bell_meditation.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/assets/bell_meditation.ogg -------------------------------------------------------------------------------- /assets/bowl_singing_big.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/assets/bowl_singing_big.ogg -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/1.png: -------------------------------------------------------------------------------- 1 | ../../../../../../media/screenshots/main.png -------------------------------------------------------------------------------- /media/screenshots/main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/media/screenshots/main.png -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule ".flutter"] 2 | path = .flutter 3 | url = https://github.com/flutter/flutter.git 4 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/6.txt: -------------------------------------------------------------------------------- 1 | * Added setting to enable Do Not Disturb while meditating 2 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/3.png: -------------------------------------------------------------------------------- 1 | ../../../../../../media/screenshots/settings.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/4.png: -------------------------------------------------------------------------------- 1 | ../../../../../../media/screenshots/settings-2.png -------------------------------------------------------------------------------- /media/icon-playstore-512px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/media/icon-playstore-512px.png -------------------------------------------------------------------------------- /media/screenshots/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/media/screenshots/settings.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/9.txt: -------------------------------------------------------------------------------- 1 | * Updated libraries 2 | * Now also available on the Play store 3 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/2.png: -------------------------------------------------------------------------------- 1 | ../../../../../../media/screenshots/time-selection.png -------------------------------------------------------------------------------- /media/screenshots/custom-time.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/media/screenshots/custom-time.png -------------------------------------------------------------------------------- /media/screenshots/settings-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/media/screenshots/settings-2.png -------------------------------------------------------------------------------- /assets/icon/icon-material-192px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/assets/icon/icon-material-192px.png -------------------------------------------------------------------------------- /assets/icon/icon-material-512px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/assets/icon/icon-material-512px.png -------------------------------------------------------------------------------- /media/screenshots/time-selection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/media/screenshots/time-selection.png -------------------------------------------------------------------------------- /media/screenshots/old/screenshot-timer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/media/screenshots/old/screenshot-timer.png -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /media/screenshots/old/screenshot-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/media/screenshots/old/screenshot-settings.png -------------------------------------------------------------------------------- /assets/icon/icon-adaptive-background-432px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/assets/icon/icon-adaptive-background-432px.png -------------------------------------------------------------------------------- /assets/icon/icon-adaptive-foreground-432px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/assets/icon/icon-adaptive-foreground-432px.png -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/android/app/src/main/res/drawable-hdpi/ic_notification.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/android/app/src/main/res/drawable-mdpi/ic_notification.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/android/app/src/main/res/drawable-xhdpi/ic_notification.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/android/app/src/main/res/drawable-xxhdpi/ic_notification.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/android/app/src/main/res/drawable-xxxhdpi/ic_notification.png -------------------------------------------------------------------------------- /android/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #fafafa 4 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | org.gradle.parallel=true 5 | org.gradle.workers.max=12 6 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/android/app/src/main/res/drawable-hdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/android/app/src/main/res/drawable-mdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/android/app/src/main/res/drawable-xhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/android/app/src/main/res/drawable-xxhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyxkn/meditation/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/17.txt: -------------------------------------------------------------------------------- 1 | + Upgraded to latest Flutter 3.24.3 2 | + Added monochrome icon for themed icons 3 | + Cleaner dismissal of end notification 4 | + Improved DND permissions request 5 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/7.txt: -------------------------------------------------------------------------------- 1 | * Added choice of custom meditation time 2 | * Added optional delay before starting the meditation timer 3 | * Added intermediate bells at user-defined intervals 4 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/5.txt: -------------------------------------------------------------------------------- 1 | * Added 20 and 25 minutes timer presets 2 | * Cleaned up permission requirements 3 | * Added fastlane metadata 4 | * Lowered minimum android API version to 21 from 23 5 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip 7 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/16.txt: -------------------------------------------------------------------------------- 1 | + Upgraded to latest Flutter 3.19.4 2 | + Migrated UI to Material 3 3 | + Updated target Android version to latest Android 14 4 | + Renamed delay timer to "Pre-meditation" delay and added an explicit checkbox 5 | + Improved the process of asking user for notification permissions 6 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 5f105a6ca7a5ac7b8bc9b241f4c2d86f4188cf5c 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Ignore warnings about missing ShortcutBadger class 2 | -dontwarn me.leolin.shortcutbadger.** 3 | 4 | # Prevent optimization or obfuscation of BadgeManager methods that reference ShortcutBadger 5 | # do we need this? it builds fine without as well 6 | -keep class me.carda.awesome_notifications.core.managers.BadgeManager { 7 | public *; 8 | } 9 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | } 7 | 8 | rootProject.buildDir = '../build' 9 | subprojects { 10 | project.buildDir = "${rootProject.buildDir}/${project.name}" 11 | } 12 | subprojects { 13 | project.evaluationDependsOn(':app') 14 | } 15 | 16 | tasks.register("clean", Delete) { 17 | delete rootProject.buildDir 18 | } 19 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return flutterSdkPath 8 | }() 9 | 10 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") 11 | 12 | repositories { 13 | google() 14 | mavenCentral() 15 | gradlePluginPortal() 16 | } 17 | } 18 | 19 | plugins { 20 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" 21 | id "com.android.application" version "8.3.2" apply false 22 | id "org.jetbrains.kotlin.android" version "2.0.20" apply false 23 | } 24 | 25 | include ":app" -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 9.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | **/ios/Flutter/.last_build_id 26 | .dart_tool/ 27 | .flutter-plugins 28 | .flutter-plugins-dependencies 29 | .packages 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Web related 35 | lib/generated_plugin_registrant.dart 36 | 37 | # Symbolication related 38 | app.*.symbols 39 | 40 | # Obfuscation related 41 | app.*.map.json 42 | 43 | # Android Studio will place build artifacts here 44 | /android/app/debug 45 | /android/app/profile 46 | /android/app/release 47 | 48 | # project 49 | *.tmp.md 50 | /assets/temp 51 | /assets-source 52 | /builds 53 | /builds-bundle 54 | /media/temp 55 | /scripts 56 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:meditation/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(const MyApp()); 17 | 18 | // Verify that our counter starts at 0. 19 | expect(find.text('0'), findsOneWidget); 20 | expect(find.text('1'), findsNothing); 21 | 22 | // Tap the '+' icon and trigger a frame. 23 | await tester.tap(find.byIcon(Icons.add)); 24 | await tester.pump(); 25 | 26 | // Verify that our counter has incremented. 27 | expect(find.text('0'), findsNothing); 28 | expect(find.text('1'), findsOneWidget); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/full_description.txt: -------------------------------------------------------------------------------- 1 | This is a meditation timer. Minimalistic, reliable, and truly elegant. 2 | 3 | Features 4 | 5 | * Simple, elegant, and intuitive 6 | * No distractions - only the essential features 7 | * Beautiful assortment of bell and gong sounds 8 | * Custom volume adjustment of the bell sounds 9 | * Reliable countdown timer 10 | * Pre-meditation delay 11 | * Intermediate meditation intervals 12 | * Free and open-source software 13 | 14 | Description 15 | 16 | Meditation is a truly minimalistic countdown timer for meditation, with a clean user interface and no clutter. The minimalism of the app embodies meditation's actual purpose. 17 | 18 | A notable feature of the app is the ability to customize the volume of the notification bells independently of system volume. This allows you to set a predefined volume so that the sounds will reliably play at the same loudness every time. No more mid-meditation worrying that you remembered to turn the volume up! 19 | 20 | Another important feature is for the timer to be as reliable as possible, to ensure an accurate meditation time and prompt ending sound notification. 21 | This is achieved by starting a foreground service and disabling battery optimization. 22 | 23 | Reliability is of extreme importance to a meditation tool in order to eliminate all possible worries about the timer not behaving correctly. Just press the button and start! 24 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options 30 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Meditation 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | Meditation 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | UIViewControllerBasedStatusBarAppearance 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /lib/audioplayer.dart: -------------------------------------------------------------------------------- 1 | import 'package:audioplayers/audioplayers.dart'; 2 | import 'package:flutter_settings_screens/flutter_settings_screens.dart'; 3 | import 'package:volume_controller/volume_controller.dart'; 4 | 5 | import 'package:meditation/settings.dart'; 6 | import 'package:meditation/utils.dart'; 7 | 8 | class NAudioPlayer { 9 | final audioPlayer = AudioPlayer(); 10 | double lastSystemVolume = 0; 11 | bool volumeHijackable = true; 12 | 13 | NAudioPlayer() { 14 | init(); 15 | } 16 | 17 | void init() { 18 | log.i("naudioplayer init"); 19 | // can only hide on android 20 | VolumeController().showSystemUI = false; 21 | } 22 | 23 | Future play(String audioFile) async { 24 | stopPrevious(); 25 | hijackVolume(); 26 | await audioPlayer.setSource(AssetSource(audioFile)); 27 | await audioPlayer.resume(); 28 | // restore volume when audio is done playing 29 | audioPlayer.onPlayerComplete.listen((_) { 30 | restoreVolume(); 31 | }); 32 | } 33 | 34 | Future stopPrevious() async { 35 | await audioPlayer.stop(); 36 | return 0; 37 | } 38 | 39 | Future playSound(String soundKey) async { 40 | int audioIndex = Settings.getValue(soundKey) ?? 0; 41 | String audioFile = audioFiles.keys.elementAt(audioIndex); 42 | await play(audioFile); 43 | } 44 | 45 | void hijackVolume() async { 46 | if (volumeHijackable) { 47 | lastSystemVolume = await VolumeController().getVolume(); 48 | volumeHijackable = false; 49 | } 50 | double volume = (Settings.getValue('volume') ?? 1.0) / 10.0; 51 | VolumeController().setVolume(volume); 52 | } 53 | 54 | void restoreVolume() { 55 | VolumeController().setVolume(lastSystemVolume); 56 | volumeHijackable = true; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | platform :ios, '12.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | end 36 | 37 | # post_install do |installer| 38 | # installer.pods_project.targets.each do |target| 39 | # flutter_additional_ios_build_settings(target) 40 | # end 41 | # end 42 | 43 | # https://stackoverflow.com/questions/37160688/set-deployment-target-for-cocoapodss-pod 44 | #platform :ios, '12.0' # set IPHONEOS_DEPLOYMENT_TARGET for the pods project 45 | post_install do |installer| 46 | installer.pods_project.targets.each do |target| 47 | target.build_configurations.each do |config| 48 | config.build_settings.delete 'IPHONEOS_DEPLOYMENT_TARGET' 49 | end 50 | flutter_additional_ios_build_settings(target) 51 | end 52 | end 53 | 54 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | {"images":[{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}} -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/nyxkn/meditation/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.nyxkn.meditation 2 | 3 | import android.app.NotificationManager 4 | import android.content.Intent 5 | import android.provider.Settings 6 | import android.os.Build 7 | import android.content.Context 8 | import androidx.annotation.NonNull 9 | 10 | import io.flutter.embedding.android.FlutterActivity 11 | import io.flutter.embedding.engine.FlutterEngine 12 | import io.flutter.plugin.common.MethodChannel 13 | 14 | class MainActivity: FlutterActivity() { 15 | override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { 16 | super.configureFlutterEngine(flutterEngine) 17 | MethodChannel( 18 | flutterEngine.dartExecutor.binaryMessenger, 19 | "com.nyxkn.meditation/channelHelper" 20 | ).setMethodCallHandler { call, result -> 21 | 22 | when (call.method) { 23 | "openChannelSettings" -> { 24 | val channelId = call.argument("channelId") 25 | channelId?.let { 26 | openChannelSettings(it) 27 | result.success(null) 28 | } ?: result.error("UNAVAILABLE", "Channel ID not available.", null) 29 | } 30 | 31 | "isChannelEnabled" -> { 32 | val channelId = call.argument("channelId") 33 | channelId?.let { 34 | result.success(isNotificationChannelEnabled(it)) 35 | } ?: result.error("UNAVAILABLE", "Channel ID not available.", null) 36 | } 37 | 38 | else -> { 39 | result.notImplemented() 40 | } 41 | } 42 | } 43 | } 44 | 45 | private fun isNotificationChannelEnabled(channelId: String): Boolean { 46 | val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager 47 | 48 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 49 | // For Android Oreo and above, check if the specific channel is enabled 50 | val channel = manager.getNotificationChannel(channelId) 51 | return channel?.importance != NotificationManager.IMPORTANCE_NONE 52 | } else { 53 | // For older versions, just assume true. Hopefully AwesomeNotifications takes care of that well enough 54 | return true; 55 | } 56 | } 57 | 58 | private fun openChannelSettings(channelId: String) { 59 | val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS).apply { 60 | putExtra(Settings.EXTRA_APP_PACKAGE, packageName) 61 | putExtra(Settings.EXTRA_CHANNEL_ID, channelId) 62 | } 63 | startActivity(intent) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - audioplayers (0.0.1): 3 | - Flutter 4 | - awesome_notifications (0.0.3): 5 | - Flutter 6 | - Flutter (1.0.0) 7 | - flutter_foreground_task (0.0.1): 8 | - Flutter 9 | - flutter_logs (0.0.1): 10 | - Flutter 11 | - ZIPFoundation 12 | - path_provider_ios (0.0.1): 13 | - Flutter 14 | - "permission_handler (5.1.0+2)": 15 | - Flutter 16 | - shared_preferences_ios (0.0.1): 17 | - Flutter 18 | - volume_controller (0.0.1): 19 | - Flutter 20 | - wakelock (0.0.1): 21 | - Flutter 22 | - ZIPFoundation (0.9.13) 23 | 24 | DEPENDENCIES: 25 | - audioplayers (from `.symlinks/plugins/audioplayers/ios`) 26 | - awesome_notifications (from `.symlinks/plugins/awesome_notifications/ios`) 27 | - Flutter (from `Flutter`) 28 | - flutter_foreground_task (from `.symlinks/plugins/flutter_foreground_task/ios`) 29 | - flutter_logs (from `.symlinks/plugins/flutter_logs/ios`) 30 | - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) 31 | - permission_handler (from `.symlinks/plugins/permission_handler/ios`) 32 | - shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`) 33 | - volume_controller (from `.symlinks/plugins/volume_controller/ios`) 34 | - wakelock (from `.symlinks/plugins/wakelock/ios`) 35 | 36 | SPEC REPOS: 37 | trunk: 38 | - ZIPFoundation 39 | 40 | EXTERNAL SOURCES: 41 | audioplayers: 42 | :path: ".symlinks/plugins/audioplayers/ios" 43 | awesome_notifications: 44 | :path: ".symlinks/plugins/awesome_notifications/ios" 45 | Flutter: 46 | :path: Flutter 47 | flutter_foreground_task: 48 | :path: ".symlinks/plugins/flutter_foreground_task/ios" 49 | flutter_logs: 50 | :path: ".symlinks/plugins/flutter_logs/ios" 51 | path_provider_ios: 52 | :path: ".symlinks/plugins/path_provider_ios/ios" 53 | permission_handler: 54 | :path: ".symlinks/plugins/permission_handler/ios" 55 | shared_preferences_ios: 56 | :path: ".symlinks/plugins/shared_preferences_ios/ios" 57 | volume_controller: 58 | :path: ".symlinks/plugins/volume_controller/ios" 59 | wakelock: 60 | :path: ".symlinks/plugins/wakelock/ios" 61 | 62 | SPEC CHECKSUMS: 63 | audioplayers: 455322b54050b30ea4b1af7cd9e9d105f74efa8c 64 | awesome_notifications: 04530aafec8dac4635244ed4b4072f4aea6cc938 65 | Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a 66 | flutter_foreground_task: 21ef182ab0a29a3005cc72cd70e5f45cb7f7f817 67 | flutter_logs: 5d0dca26963e0f30b11a995a764744ef77d5d428 68 | path_provider_ios: 7d7ce634493af4477d156294792024ec3485acd5 69 | permission_handler: ccb20a9fad0ee9b1314a52b70b76b473c5f8dab0 70 | shared_preferences_ios: aef470a42dc4675a1cdd50e3158b42e3d1232b32 71 | volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9 72 | wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f 73 | ZIPFoundation: ae5b4b813d216d3bf0a148773267fff14bd51d37 74 | 75 | PODFILE CHECKSUM: d1dc31c2e2bfaf9a19bf9981bc2885279fa137d7 76 | 77 | COCOAPODS: 1.11.2 78 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 19 | 20 | 25 | 26 | 38 | 41 | 45 | 48 | 49 | 50 | 51 | 52 | 53 | 55 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: meditation 2 | description: Meditation timer 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `flutter pub publish`. This is preferred for private packages. 6 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 7 | 8 | # The following defines the version and build number for your application. 9 | # A version number is three numbers separated by dots, like 1.2.43 10 | # followed by an optional build number separated by a +. 11 | # Both the version and the builder number may be overridden in flutter 12 | # build by specifying --build-name and --build-number, respectively. 13 | # In Android, build-name is used as versionName while build-number used as versionCode. 14 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 15 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 16 | # Read more about iOS versioning at 17 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 18 | version: 1.5.0+17 19 | 20 | environment: 21 | sdk: ^3.5.3 22 | 23 | 24 | # Dependencies specify other packages that your package needs in order to work. 25 | # To automatically upgrade your package dependencies to the latest versions 26 | # consider running `flutter pub upgrade --major-versions`. Alternatively, 27 | # dependencies can be manually updated by changing the version numbers below to 28 | # the latest version available on pub.dev. To see which dependencies have newer 29 | # versions available, run `flutter pub outdated`. 30 | dependencies: 31 | flutter: 32 | sdk: flutter 33 | 34 | # Project dependencies 35 | audioplayers: ^6.0.0 36 | awesome_notifications: ^0.10.0 # receive_boot_completed, vibrate 37 | flutter_background: ^1.1.0 # foreground_service, wake_lock 38 | do_not_disturb: ^1.0.1 39 | # flutter_foreground_task: ^3.5.5 40 | # flutter_settings_screens: 0.3.3-null-safety+2 41 | get_it: ^8.0.0 42 | logger: ^2.1.0 43 | # permission_handler: ^8.3.0 44 | shared_preferences: ^2.0.12 45 | volume_controller: ^2.0.3 46 | wakelock_plus: ^1.2.8 47 | package_info_plus: ^8.0.3 48 | event_bus: ^2.0.0 49 | app_settings: ^5.1.1 50 | 51 | flutter_settings_screens: 52 | # path: /home/nyxkn/apps/flutter-projects/flutter_settings_screens 53 | git: 54 | url: https://github.com/GAM3RG33K/flutter_settings_screens.git 55 | ref: 18f9377 56 | 57 | 58 | dev_dependencies: 59 | flutter_test: 60 | sdk: flutter 61 | 62 | flutter_launcher_icons: ^0.14.1 63 | 64 | # The "flutter_lints" package below contains a set of recommended lints to 65 | # encourage good coding practices. The lint set provided by the package is 66 | # activated in the `analysis_options.yaml` file located at the root of your 67 | # package. See that file for information about deactivating specific lint 68 | # rules and activating additional ones. 69 | flutter_lints: ^5.0.0 70 | 71 | 72 | # to generate icons: 73 | # flutter pub run flutter_launcher_icons:main 74 | flutter_icons: 75 | android: true 76 | ios: true 77 | image_path_android: "assets/icon/icon-material-192px.png" 78 | image_path_ios: "assets/icon/icon-material-192px.png" 79 | adaptive_icon_background: "#fafafa" 80 | adaptive_icon_foreground: "assets/icon/icon-adaptive-foreground-432px.png" 81 | adaptive_icon_monochrome: "assets/icon/icon-adaptive-foreground-432px.png" 82 | 83 | # For information on the generic Dart part of this file, see the 84 | # following page: https://dart.dev/tools/pub/pubspec 85 | 86 | # The following section is specific to Flutter. 87 | flutter: 88 | 89 | # The following line ensures that the Material Icons font is 90 | # included with your application, so that you can use the icons in 91 | # the material Icons class. 92 | uses-material-design: true 93 | 94 | # To add assets to your application, add an assets section, like this: 95 | # assets: 96 | # - images/a_dot_burr.jpeg 97 | # - images/a_dot_ham.jpeg 98 | 99 | assets: 100 | - assets/ 101 | # - android/app/src/main/res/mipmap-xxxhdpi/ 102 | # - android/app/src/main/res/drawable-xxxhdpi/ 103 | 104 | # An image asset can refer to one or more resolution-specific "variants", see 105 | # https://flutter.dev/assets-and-images/#resolution-aware. 106 | 107 | # For details regarding adding assets from package dependencies, see 108 | # https://flutter.dev/assets-and-images/#from-packages 109 | 110 | # To add custom fonts to your application, add a fonts section here, 111 | # in this "flutter" section. Each entry in this list should have a 112 | # "family" key with the font family name, and a "fonts" key with a 113 | # list giving the asset and other descriptors for the font. For 114 | # example: 115 | # fonts: 116 | # - family: Schyler 117 | # fonts: 118 | # - asset: fonts/Schyler-Regular.ttf 119 | # - asset: fonts/Schyler-Italic.ttf 120 | # style: italic 121 | # - family: Trajan Pro 122 | # fonts: 123 | # - asset: fonts/TrajanPro.ttf 124 | # - asset: fonts/TrajanPro_Bold.ttf 125 | # weight: 700 126 | # 127 | # For details regarding fonts from package dependencies, 128 | # see https://flutter.dev/custom-fonts/#from-packages -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | id "kotlin-android" 4 | id "dev.flutter.flutter-gradle-plugin" 5 | } 6 | 7 | def localProperties = new Properties() 8 | def localPropertiesFile = rootProject.file('local.properties') 9 | if (localPropertiesFile.exists()) { 10 | localPropertiesFile.withReader('UTF-8') { reader -> 11 | localProperties.load(reader) 12 | } 13 | } 14 | 15 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 16 | if (flutterVersionCode == null) { 17 | flutterVersionCode = '1' 18 | } 19 | 20 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 21 | if (flutterVersionName == null) { 22 | flutterVersionName = '1.0' 23 | } 24 | 25 | // https://docs.flutter.dev/deployment/android 26 | def keystoreProperties = new Properties() 27 | def keystorePropertiesFile = rootProject.file('key.properties') 28 | if (keystorePropertiesFile.exists()) { 29 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) 30 | } 31 | 32 | android { 33 | // def minSdk = 21 // because of a bug, you need to manually set this below 34 | def targetSdk = 34 35 | 36 | namespace "com.nyxkn.meditation" 37 | compileSdkVersion targetSdk 38 | ndkVersion "25.1.8937393" 39 | 40 | compileOptions { 41 | // sourceCompatibility JavaVersion.VERSION_1_8 42 | // targetCompatibility JavaVersion.VERSION_1_8 43 | sourceCompatibility JavaVersion.VERSION_17 44 | targetCompatibility JavaVersion.VERSION_17 45 | } 46 | 47 | kotlinOptions { 48 | jvmTarget = 17 49 | } 50 | 51 | sourceSets { 52 | main.java.srcDirs += 'src/main/kotlin' 53 | } 54 | 55 | defaultConfig { 56 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 57 | applicationId "com.nyxkn.meditation" 58 | // FIXME: a bug in flutter_launcher_icons requires min Sdk Version to be specified as a number 59 | // rather than using the variable minSdk. we can't even use the word min Sdk Version in comments 60 | // because it just naively looks for any line containing the string. even if it's a comment! 61 | // https://github.com/fluttercommunity/flutter_launcher_icons/issues/324 62 | minSdkVersion 23 // lollipop 6.0, 98.8% 63 | targetSdkVersion targetSdk 64 | versionCode flutterVersionCode.toInteger() 65 | versionName flutterVersionName 66 | } 67 | 68 | signingConfigs { 69 | release { 70 | keyAlias keystoreProperties['keyAlias'] 71 | keyPassword keystoreProperties['keyPassword'] 72 | storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null 73 | storePassword keystoreProperties['storePassword'] 74 | } 75 | } 76 | buildTypes { 77 | release { 78 | signingConfig signingConfigs.release 79 | shrinkResources = false 80 | ndk { 81 | debugSymbolLevel 'FULL' 82 | } 83 | } 84 | } 85 | 86 | // buildTypes { 87 | // release { 88 | // // TODO: Add your own signing config for the release build. 89 | // // Signing with the debug keys for now, so `flutter run --release` works. 90 | // signingConfig signingConfigs.debug 91 | // } 92 | // } 93 | } 94 | 95 | flutter { 96 | source '../..' 97 | } 98 | 99 | dependencies { 100 | // implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 101 | } 102 | 103 | configurations.all { 104 | // this removes the module from the build (and all the permissions related to badges) 105 | // see also corresponding proguard rules 106 | exclude group: 'me.leolin', module: 'ShortcutBadger' 107 | } 108 | 109 | 110 | // by default we get arm 1000 + version, arm64 2000, x86_64 4000 111 | // this changes the scheme to version * 100 112 | // +1 for x86_64, +2 for arm, +3 for arm64, e.g. 1103 is version 11 arm64 113 | 114 | // code from here: https://developer.android.com/studio/build/configure-apk-splits#configure-APK-versions 115 | // more versioning ideas: https://developer.android.com/google/play/publishing/multiple-apks#VersionCodes 116 | 117 | // Map for the version code that gives each ABI a value. 118 | // 0 is reserved for the fat apk 119 | // newer standards get a higher number (to allow possible upgrade path) 120 | ext.abiCodes = ['x86_64': 1, 'armeabi-v7a': 2, 'arm64-v8a': 3] 121 | // For per-density APKs, create a similar map: 122 | // ext.densityCodes = ['mdpi': 1, 'hdpi': 2, 'xhdpi': 3] 123 | 124 | import com.android.build.OutputFile 125 | 126 | // For each APK output variant, override versionCode with a combination of 127 | // ext.abiCodes * 1000 + variant.versionCode. In this example, variant.versionCode 128 | // is equal to defaultConfig.versionCode. If you configure product flavors that 129 | // define their own versionCode, variant.versionCode uses that value instead. 130 | android.applicationVariants.all { variant -> 131 | 132 | // Assigns a different version code for each output APK 133 | // other than the universal APK. 134 | variant.outputs.each { output -> 135 | 136 | // Stores the value of ext.abiCodes that is associated with the ABI for this variant. 137 | def baseAbiVersionCode = 138 | // Determines the ABI for this variant and returns the mapped value. 139 | project.ext.abiCodes.get(output.getFilter(OutputFile.ABI)) 140 | 141 | // Because abiCodes.get() returns null for ABIs that are not mapped by ext.abiCodes, 142 | // the following code doesn't override the version code for universal APKs. 143 | // However, because you want universal APKs to have the lowest version code, 144 | // this outcome is desirable. 145 | if (baseAbiVersionCode != null) { 146 | 147 | // Assigns the new version code to versionCodeOverride, which changes the 148 | // version code for only the output APK, not for the variant itself. Skipping 149 | // this step causes Gradle to use the value of variant.versionCode for the APK. 150 | // output.versionCodeOverride = baseAbiVersionCode * 1000 + variant.versionCode 151 | 152 | // my own version: 153 | output.versionCodeOverride = variant.versionCode * 100 + baseAbiVersionCode 154 | } else { 155 | // fat apk. should be the lowest version, so you can always upgrade to specific abi 156 | output.versionCodeOverride = variant.versionCode * 100 + 0 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Meditation 2 | 3 | 4 | 5 | 6 | *This is a meditation timer. Minimalistic, reliable, and truly elegant.* 7 | 8 | [Get it on F-Droid](https://f-droid.org/packages/com.nyxkn.meditation/) 11 | 12 | [Get it on Google Play](https://play.google.com/store/apps/details?id=com.nyxkn.meditation) 15 | 16 | Or download the APK directly from GitHub: 17 | 18 | - **[Latest release](https://github.com/nyxkn/meditation/releases/latest)** 19 | 20 | ## Features 21 | 22 | ![Shield: GitHub version](https://img.shields.io/github/v/release/nyxkn/meditation?style=for-the-badge&logo=github) 23 | ![Shield: F-droid version](https://img.shields.io/f-droid/v/com.nyxkn.meditation?style=for-the-badge&logo=fdroid) 24 | 25 | * Simple, elegant, and intuitive 26 | * No distractions - only the essential features 27 | * Beautiful assortment of bell and gong sounds 28 | * Custom volume adjustment of the bell sounds 29 | * Reliable countdown timer 30 | * Pre-meditation delay 31 | * Intermediate meditation intervals 32 | * Free and open-source software 33 | 34 | ## Screenshots 35 | 36 | 37 |   38 | 39 |   40 | 41 |   42 | 43 | 44 | ## Description 45 | 46 | Meditation is a truly minimalistic countdown timer for meditation, with a clean user interface and no clutter. The minimalism of the app embodies meditation's actual purpose. 47 | 48 | A notable feature of the app is the ability to customize the volume of the notification bells independently of system volume. This allows you to set a predefined volume so that the sounds will reliably play at the same loudness every time. No more mid-meditation worrying that you remembered to turn the volume up! 49 | 50 | Another important feature is for the timer to be as reliable as possible, to ensure an accurate meditation time and prompt ending sound notification. 51 | This is achieved by starting a foreground service and disabling battery optimization. 52 | 53 | Reliability is of extreme importance to a meditation tool in order to eliminate all possible worries about the timer not behaving correctly. Just press the button and start! 54 | 55 | ## Donations 56 | 57 | If you find this project helpful and you feel like it, throw me some coins! 58 | 59 | And drop a [star](stargazers) on this repo :) 60 | 61 | [![Buy me a coffee](https://img.shields.io/badge/buy%20me%20a%20coffee-FFDD00?style=for-the-badge&logo=buymeacoffee&logoColor=black)](https://www.buymeacoffee.com/nyxkn) 62 | 63 | [![Liberapay](https://img.shields.io/badge/donate-liberapay-F6C915?style=for-the-badge&logo=liberapay&logoColor=)](https://liberapay.com/nyxkn) 64 | 65 | [![Support me on ko-fi](https://img.shields.io/badge/support%20me%20on%20ko--fi-FF5E5B?style=for-the-badge&logo=kofi&logoColor=black)](https://ko-fi.com/nyxkn) 66 | 67 | [![Paypal](https://img.shields.io/badge/donate-paypal-00457C?style=for-the-badge&logo=paypal&logoColor=)](https://paypal.me/nicolasiagri) 68 | 69 | Bitcoin: bc1qfu5gk78898zdcxw372unmwua0yd5luf3z60sgq 70 | 71 | ## Technical notes 72 | 73 | An important feature to implement was to have the volume of the notification bells be consistent and reliable. 74 | In other apps, the volume is tied to the system volume, implemented either as using the system volume setting directly, or as a modifier thereof. 75 | Both approaches are flawed and will lead to inconsistent volumes if the system volume changes or if you had forgotten to set it to the desired level before starting the timer. 76 | With my approach the app makes use of a configurable absolute value, so that the sounds play consistently at the same volume no matter what. 77 | 78 | Another issue to solve was the reliability of the timer timeout event, making sure it happens at exactly the right time. 79 | For whatever reason, this is a ridiculously complex problem on mobile devices. There are continuous "improvements" to attempt to extend battery life, that make it really hard to ensure that a time-critical task happens at the right time. 80 | This app will make use of all possible tricks to ensure it is reliable. What I settled on was a combination of a foreground service and disabling battery optimization. This allows notifications to be sent reliably at the correct time. If that still doesn't work, you also have the option of keeping the screen on through wakelock. 81 | 82 | ## Credits 83 | 84 | ### Audio files 85 | 86 | Here's a listing of the original audio files that each asset was derived from. 87 | They were all modified to improve cohesion. 88 | 89 | - bell_burma: (CC0) 90 | - bell_indian: (CC Sampling Plus 1.0) 91 | - bell_meditation: (CC0) 92 | - bell_singing: (CC0) 93 | - bowl_singing_big: (CC0) 94 | - bowl_singing: (CC-BY 3.0) 95 | - gong_bodhi: (origin unclear) 96 | - gong_generated: (CC0) 97 | - gong_watts: (origin unclear; possibly from the "Alan Watts Guided Meditation" audio) 98 | 99 | ### Other 100 | 101 | - [Enso.svg](https://commons.wikimedia.org/wiki/File:Enso.svg) (CC0): used in the making of the app icons. 102 | 103 | ## License 104 | 105 | ### Source code 106 | 107 | All source code is licensed under the [GPL-3.0-only License](https://spdx.org/licenses/GPL-3.0-only.html). 108 | 109 | > This program is [free software](https://www.gnu.org/philosophy/free-sw.html): you can redistribute it and/or modify it under the terms of the [GNU General Public License](https://www.gnu.org/licenses/gpl-3.0.en.html) as published by the Free Software Foundation, version 3. 110 | 111 | ### Assets 112 | 113 | All assets (images and audio files) are licensed under the [CC-BY-SA 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/). 114 | 115 | This includes everything in the *assets* and *media* folders and in *android/app/src/main/res*. 116 | 117 | ### Third-party 118 | 119 | This project is developed using the [Flutter framework](https://flutter.dev/), which is licensed under the [BSD 3-Clause License](https://github.com/flutter/flutter/blob/master/LICENSE). 120 | 121 | Additional licensing information on all of the Flutter modules that are being used can be found in the in-app *About* screen. 122 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/services.dart'; 4 | 5 | import 'package:awesome_notifications/awesome_notifications.dart'; 6 | import 'package:flutter_background/flutter_background.dart'; 7 | import 'package:flutter_settings_screens/flutter_settings_screens.dart'; 8 | import 'package:get_it/get_it.dart'; 9 | import 'package:logger/logger.dart'; 10 | import 'package:shared_preferences/shared_preferences.dart'; 11 | import 'package:event_bus/event_bus.dart'; 12 | import 'package:app_settings/app_settings.dart'; 13 | 14 | import 'package:meditation/audioplayer.dart'; 15 | import 'package:meditation/settings.dart'; 16 | import 'package:meditation/timer.dart'; 17 | import 'package:meditation/utils.dart'; 18 | 19 | final EventBus eventBus = EventBus(sync: true); 20 | 21 | class NotificationEvent { 22 | String type; 23 | int? id; 24 | 25 | NotificationEvent(this.type, this.id); 26 | } 27 | 28 | class DynamicEvent { 29 | dynamic data; 30 | 31 | DynamicEvent(this.data); 32 | } 33 | 34 | // on the very first call to initialize, android will ask to allow notifications 35 | // there is no way to retrigger this popup, and instead you'll have to redirect user to the settings 36 | // so call init once on first start 37 | // then have a function to check notifications 38 | // and if notifications aren't enabled, show information popup and redirect to settings 39 | Future initNotifications() async { 40 | // initialize and set a default channel 41 | bool success = await AwesomeNotifications().initialize( 42 | // set the icon to null if you want to use the default app icon 43 | // null, 44 | 'resource://drawable/ic_notification', 45 | [ 46 | NotificationChannel( 47 | channelKey: 'timer-main', 48 | channelName: 'Timer notifications', 49 | channelDescription: 'Critical notifications displayed during meditation', 50 | defaultColor: secondaryColor, 51 | importance: NotificationImportance.Max, 52 | // critical alerts are to play sound and vibration even in dnd. we might not actually need this 53 | criticalAlerts: true, 54 | playSound: false, 55 | enableVibration: false, 56 | // we need lights so that we can check if channel is enabled 57 | enableLights: true, 58 | ), 59 | NotificationChannel( 60 | channelKey: 'timer-support', 61 | channelName: 'Timer (support)', 62 | channelDescription: 'Support notifications. Not visible but must stay enabled', 63 | defaultColor: secondaryColor, 64 | // urgent: makes sound and appears as heads up 65 | // high/default: makes sound 66 | // medium/low: no sound 67 | // min: no sound and does not appear in status bar 68 | // importance: NotificationImportance.Min, 69 | importance: NotificationImportance.Default, 70 | criticalAlerts: false, 71 | playSound: false, 72 | enableVibration: false, 73 | enableLights: true, 74 | ), 75 | ], 76 | // Channel groups are only visual and are not required 77 | // channelGroups: [ 78 | // NotificationChannelGroup( 79 | // channelGroupkey: 'default-channel-group', channelGroupName: 'Default group') 80 | // ], 81 | // debug: true 82 | ); 83 | log.i('awesome_notifications init: success = $success'); 84 | } 85 | 86 | Future initFlutterBackground() async { 87 | // if (!Platform.isAndroid) { 88 | // return; 89 | // } 90 | 91 | const androidConfig = FlutterBackgroundAndroidConfig( 92 | notificationTitle: "Meditation in progress", 93 | notificationText: "Foreground service notification to keep the app running", 94 | notificationImportance: AndroidNotificationImportance.normal, 95 | notificationIcon: AndroidResource( 96 | name: 'ic_notification', defType: 'drawable'), // Default is ic_launcher from folder mipmap 97 | ); 98 | 99 | bool success = await FlutterBackground.initialize(androidConfig: androidConfig); 100 | log.i('flutter_background init: success = $success'); 101 | } 102 | 103 | Future initDefaultSettings() async { 104 | // in case there's no stored value for a setting, set a default one 105 | // this happens on a fresh install (or if you add a setting) 106 | // do not rely on the settingstile default value. that seems to only be visual 107 | 108 | if (Settings.getValue('volume') == null) { 109 | await Settings.setValue('volume', 6); 110 | } 111 | 112 | if (Settings.getValue('start-sound') == null) { 113 | var startSoundId = audioFiles.values.toList().indexOf('Singing Bowl'); 114 | await Settings.setValue('start-sound', startSoundId); 115 | } 116 | 117 | if (Settings.getValue('end-sound') == null) { 118 | var endSoundId = audioFiles.values.toList().indexOf('Burma Bell'); 119 | await Settings.setValue('end-sound', endSoundId); 120 | } 121 | 122 | if (Settings.getValue('interval-sound') == null) { 123 | var intervalSoundId = audioFiles.values.toList().indexOf('Meditation Bell'); 124 | await Settings.setValue('interval-sound', intervalSoundId); 125 | } 126 | 127 | if (Settings.getValue('interval-time') == null) { 128 | await Settings.setValue('interval-time', '5'); 129 | } 130 | 131 | if (Settings.getValue('show-countdown') == null) { 132 | await Settings.setValue('show-countdown', true); 133 | } 134 | 135 | if (Settings.getValue('delay-enabled') == null) { 136 | await Settings.setValue('delay-enabled', false); 137 | } 138 | 139 | if (Settings.getValue('delay-time') == null) { 140 | await Settings.setValue('delay-time', '5'); 141 | } 142 | } 143 | 144 | Future initDefaultPrefs() async { 145 | final prefs = await SharedPreferences.getInstance(); 146 | 147 | // setting defaults in case there's no stored value (on fresh install or new setting) 148 | 149 | if ((prefs.getInt('timer-minutes') ?? -1) == -1) { 150 | prefs.setInt('timer-minutes', 15); 151 | } 152 | } 153 | 154 | void main() async { 155 | // Be sure to add this line if initialize() call happens before runApp() 156 | WidgetsFlutterBinding.ensureInitialized(); 157 | 158 | SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); 159 | 160 | GetIt.I.registerSingleton(NAudioPlayer()); 161 | GetIt.I.registerSingleton(Logger()); 162 | 163 | await Settings.init(); 164 | 165 | await initDefaultSettings(); 166 | await initDefaultPrefs(); 167 | 168 | runApp(const MyApp()); 169 | } 170 | 171 | class MyApp extends StatelessWidget { 172 | const MyApp({Key? key}) : super(key: key); 173 | 174 | @override 175 | Widget build(BuildContext context) { 176 | var blackTheme = false; 177 | var bgColor = blackTheme ? Colors.black : backgroundColor; // quick and dirty overshadowing 178 | 179 | return MaterialApp( 180 | title: 'Meditation Timer', 181 | themeMode: ThemeMode.dark, 182 | theme: ThemeData( 183 | colorScheme: ColorScheme.fromSeed(seedColor: primaryColor!, brightness: Brightness.dark) 184 | .copyWith(primary: primaryColor, surface: Colors.black, secondary: primaryColor), 185 | scaffoldBackgroundColor: bgColor, 186 | typography: Typography.material2021(), 187 | canvasColor: blackTheme ? Colors.black : surfaceColor, 188 | dialogBackgroundColor: surfaceColor, 189 | snackBarTheme: SnackBarThemeData( 190 | backgroundColor: surfaceColor, 191 | contentTextStyle: TextStyle(color: Colors.white), 192 | ), 193 | progressIndicatorTheme: ProgressIndicatorThemeData( 194 | // circularTrackColor: Colors.black, 195 | circularTrackColor: surfaceColor, 196 | ), 197 | textButtonTheme: TextButtonThemeData( 198 | style: TextButton.styleFrom( 199 | backgroundColor: bgColor, 200 | foregroundColor: Colors.white, 201 | ), 202 | ), 203 | textTheme: Typography().white.copyWith( 204 | // countdown timer 205 | bodyLarge: Typography().white.bodyLarge?.copyWith( 206 | // fontSizeFactor: 1.2, 207 | fontSize: 18, 208 | color: Colors.grey[500], 209 | // fontWeight: FontWeight.w400, 210 | ), 211 | // times selection 212 | bodyMedium: Typography().white.bodyMedium?.copyWith( 213 | // fontSizeFactor: 1.2, 214 | fontSize: 16, 215 | letterSpacing: 0.5, 216 | height: 2, 217 | color: Colors.white, 218 | ), 219 | )), 220 | home: const Home(), 221 | ); 222 | } 223 | } 224 | 225 | TextStyle? largeButtonsTextStyle = Typography().white.labelLarge?.copyWith( 226 | fontSize: 22, 227 | fontWeight: FontWeight.w900, 228 | letterSpacing: 1.8, 229 | ); 230 | 231 | ButtonStyle timerButtonStyle = TextButton.styleFrom( 232 | textStyle: largeButtonsTextStyle, 233 | shape: CircleBorder(), 234 | ); 235 | 236 | ButtonStyle timeSelectionButtonStyle = TextButton.styleFrom( 237 | textStyle: largeButtonsTextStyle, 238 | ); 239 | 240 | class Home extends StatelessWidget { 241 | const Home({Key? key}) : super(key: key); 242 | 243 | @override 244 | Widget build(BuildContext context) { 245 | WidgetsBinding.instance.addPostFrameCallback((_) => afterBuild(context)); 246 | 247 | return Scaffold( 248 | appBar: AppBar( 249 | title: Text('Meditation Timer'), 250 | actions: [ 251 | IconButton( 252 | icon: const Icon( 253 | Icons.settings_outlined, 254 | ), 255 | onPressed: () { 256 | Navigator.push( 257 | context, 258 | MaterialPageRoute(builder: (context) => const SettingsWidget()), 259 | ); 260 | }, 261 | ), 262 | ], 263 | ), 264 | body: Center( 265 | child: TimerWidget(), 266 | ), 267 | floatingActionButton: kReleaseMode 268 | ? null 269 | : FloatingActionButton( 270 | onPressed: () { 271 | AppSettings.openAppSettings(type: AppSettingsType.settings); 272 | }, 273 | ), 274 | ); 275 | } 276 | 277 | void afterBuild(context) async { 278 | await Future.delayed(Duration(milliseconds: 500)); 279 | // this will ask for permission if first app run 280 | initNotifications(); 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /lib/settings.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter/services.dart'; 4 | 5 | import 'package:do_not_disturb/do_not_disturb.dart'; 6 | import 'package:flutter_settings_screens/flutter_settings_screens.dart'; 7 | import 'package:get_it/get_it.dart'; 8 | import 'package:package_info_plus/package_info_plus.dart'; 9 | 10 | import 'package:meditation/audioplayer.dart'; 11 | import 'package:meditation/utils.dart'; 12 | 13 | const Map audioFiles = { 14 | 'bell_burma.ogg': 'Burma Bell', 15 | 'bell_burma_three.ogg': 'Three Burma Bells', 16 | 'bell_indian.ogg': 'Indian Bell', 17 | 'bell_meditation.ogg': 'Meditation Bell', 18 | 'bell_singing.ogg': 'Singing Bell', 19 | // 'bell_zen.ogg': 'Zen Bell', 20 | 'bowl_singing.ogg': 'Singing Bowl', 21 | 'bowl_singing_big.ogg': 'Big Singing Bowl', 22 | // 'bowl_tibetan.ogg': 'Tibetan Bowl', 23 | 'gong_bodhi.ogg': 'Gong', 24 | 'gong_generated.ogg': 'Generated Gong', 25 | // 'gong_metal.ogg': 'Metal Gong', 26 | 'gong_watts.ogg': 'Alan Watts Gong', 27 | }; 28 | 29 | class SettingsWidget extends StatefulWidget { 30 | const SettingsWidget({Key? key}) : super(key: key); 31 | 32 | @override 33 | _SettingsWidgetState createState() => _SettingsWidgetState(); 34 | } 35 | 36 | class _SettingsWidgetState extends State { 37 | @override 38 | Widget build(BuildContext context) { 39 | Map soundsValues = {}; 40 | for (var i = 0; i < audioFiles.length; i++) { 41 | soundsValues[i] = audioFiles.values.elementAt(i); 42 | } 43 | 44 | final dndPlugin = DoNotDisturbPlugin(); 45 | 46 | final GetIt getIt = GetIt.instance; 47 | final NAudioPlayer audioPlayer = getIt.get(); 48 | 49 | double lastVolumeValue = -1; 50 | int lastStartSoundValue = -1; 51 | int lastEndSoundValue = -1; 52 | 53 | return Scaffold( 54 | appBar: AppBar( 55 | title: const Text('Settings'), 56 | ), 57 | body: Center( 58 | // child: Column( 59 | child: ListView( 60 | // mainAxisSize: MainAxisSize.min, 61 | children: [ 62 | SettingsGroup( 63 | title: 'volume', 64 | children: [ 65 | SliderSettingsTile( 66 | title: 'Bells volume', 67 | settingKey: 'volume', 68 | min: 0, 69 | max: 10, 70 | step: 1, 71 | leading: Icon(Icons.volume_up), 72 | onChange: (value) { 73 | if (value != lastVolumeValue) { 74 | audioPlayer.playSound('end-sound'); 75 | } 76 | lastVolumeValue = value; 77 | }, 78 | ), 79 | ], 80 | ), 81 | SettingsGroup(title: 'Options', children: [ 82 | // CheckboxSettingsTile(title: 'Hide timer countdown', settingKey: 'hide-countdown'), 83 | CheckboxSettingsTile( 84 | title: 'Keep screen on', 85 | settingKey: 'screen-wakelock', 86 | subtitle: "Enable this to keep the screen on as you meditate", 87 | ), 88 | CheckboxSettingsTile( 89 | title: 'Do not disturb', 90 | settingKey: 'dnd', 91 | subtitle: "Enable 'Do Not Disturb' mode while meditating", 92 | onChange: (value) async { 93 | if (value == true) { 94 | bool hasAccess = await dndPlugin.isNotificationPolicyAccessGranted(); 95 | if (!hasAccess) { 96 | await requestPermissionDND(context, dndPlugin); 97 | 98 | // we have no choice but to set this to false 99 | // and let the user reenable the checkbox when they come back from settings screen 100 | // that's because we send them to settings page but our code continues. we can't await 101 | // notify=true ensures the page gets reloaded to show the change 102 | await Settings.setValue('dnd', false, notify: true); 103 | } 104 | } 105 | }, 106 | ), 107 | CheckboxSettingsTile( 108 | title: 'Show countdown', 109 | settingKey: 'show-countdown', 110 | subtitle: "Show the remaining meditation time", 111 | ), 112 | CheckboxSettingsTile( 113 | settingKey: 'delay-enabled', 114 | title: 'Pre-meditation delay', 115 | subtitle: 'Add an initial delay before the meditation starts', 116 | childrenIfEnabled: [ 117 | Divider(height: 4, thickness: 4, indent: 0), 118 | Container( 119 | padding: EdgeInsets.only(left: 8.0), 120 | child: Column( 121 | children: [ 122 | TextInputSettingsTile( 123 | title: 'Delay duration (seconds)', 124 | settingKey: 'delay-time', 125 | keyboardType: TextInputType.number, 126 | selectAllOnFocus: true, 127 | autovalidateMode: AutovalidateMode.onUserInteraction, 128 | validator: 129 | timeInputValidatorConstructor(minTimerTime: 5, maxTimerTime: 60) 130 | as Validator, 131 | inputFormatters: [ 132 | FilteringTextInputFormatter.digitsOnly, 133 | ], 134 | borderColor: primaryColor, 135 | errorColor: secondaryColor, 136 | helperText: 'Delay time in seconds, between 5 and 60.', 137 | ), 138 | ], 139 | )) 140 | ]) 141 | ]), 142 | SettingsGroup( 143 | title: 'Sounds', 144 | children: [ 145 | RadioModalSettingsTile( 146 | title: 'Start sound', 147 | settingKey: 'start-sound', 148 | selected: soundsValues.keys.first, 149 | values: soundsValues, 150 | onChange: (value) { 151 | if (value != lastStartSoundValue) { 152 | audioPlayer.play(audioFiles.keys.elementAt(value)); 153 | } 154 | lastStartSoundValue = value; 155 | }, 156 | ), 157 | RadioModalSettingsTile( 158 | title: 'End sound', 159 | settingKey: 'end-sound', 160 | selected: soundsValues.keys.elementAt(1), 161 | values: soundsValues, 162 | onChange: (value) { 163 | if (value != lastEndSoundValue) { 164 | audioPlayer.play(audioFiles.keys.elementAt(value)); 165 | } 166 | lastEndSoundValue = value; 167 | }, 168 | ), 169 | ], 170 | ), 171 | SettingsGroup( 172 | title: 'Intervals', 173 | children: [ 174 | CheckboxSettingsTile( 175 | settingKey: 'intervals-enabled', 176 | title: 'Enable intervals', 177 | subtitle: 'Intermediate bells during your meditation', 178 | childrenIfEnabled: [ 179 | Divider(height: 4, thickness: 4, indent: 0), 180 | Container( 181 | padding: EdgeInsets.only(left: 8.0), 182 | child: Column( 183 | children: [ 184 | TextInputSettingsTile( 185 | title: 'Interval duration (minutes)', 186 | settingKey: 'interval-time', 187 | keyboardType: TextInputType.number, 188 | selectAllOnFocus: true, 189 | autovalidateMode: AutovalidateMode.onUserInteraction, 190 | validator: timeInputValidatorConstructor( 191 | minTimerTime: 1, maxTimerTime: maxMeditationTime) as Validator, 192 | inputFormatters: [ 193 | FilteringTextInputFormatter.digitsOnly, 194 | FilteringTextInputFormatter.deny(RegExp(r"^0")), 195 | ], 196 | borderColor: primaryColor, 197 | errorColor: secondaryColor, 198 | helperText: 199 | 'Interval time in minutes, between 1 and $maxMeditationTime.', 200 | ), 201 | // Divider( 202 | // height: 1, 203 | // thickness: 1, 204 | // indent: 8, 205 | // ), 206 | RadioModalSettingsTile( 207 | title: 'Interval sound', 208 | settingKey: 'interval-sound', 209 | selected: soundsValues.keys.first, 210 | values: soundsValues, 211 | onChange: (value) { 212 | if (value != lastStartSoundValue) { 213 | audioPlayer.play(audioFiles.keys.elementAt(value)); 214 | } 215 | lastStartSoundValue = value; 216 | }, 217 | ), 218 | SizedBox(height: 4), 219 | ], 220 | ), 221 | ), 222 | ], 223 | ), 224 | ], 225 | ), 226 | SettingsGroup( 227 | title: 'Info', 228 | children: [ 229 | SizedBox(height: 16), 230 | SimpleSettingsTile( 231 | title: 'About', 232 | subtitle: 'Licences and other information', 233 | onTap: () async { 234 | final PackageInfo packageInfo = await PackageInfo.fromPlatform(); 235 | showAboutDialog( 236 | context: context, 237 | applicationName: packageInfo.appName, 238 | applicationVersion: packageInfo.version + ' (${packageInfo.buildNumber})', 239 | applicationLegalese: 240 | "${packageInfo.packageName}\n\nA meditation timer made with Flutter.", 241 | ); 242 | }, 243 | ), 244 | ], 245 | ), 246 | if (!kReleaseMode) 247 | SettingsGroup( 248 | title: 'Debug', 249 | children: [], 250 | ), 251 | ], 252 | ), 253 | ), 254 | ); 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /lib/utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:awesome_notifications/awesome_notifications.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_background/flutter_background.dart'; 6 | import 'package:do_not_disturb/do_not_disturb.dart'; 7 | import 'package:intl/intl.dart'; 8 | import 'package:flutter/services.dart'; 9 | import 'package:flutter_settings_screens/flutter_settings_screens.dart'; 10 | 11 | import 'package:meditation/main.dart'; 12 | import 'package:logger/logger.dart'; 13 | 14 | // ======================================== 15 | // globals 16 | // ======================================== 17 | 18 | const maxMeditationTime = 300; 19 | 20 | final backgroundColor = Color(0xFF121212); // very dark gray, darker than grey[900] 21 | final surfaceColor = Colors.grey[900]; 22 | final primaryColor = Colors.indigoAccent[100]; 23 | final secondaryColor = Colors.redAccent[100]; // we use this as our shade of red for errors 24 | 25 | // ======================================== 26 | // utilities 27 | // ======================================== 28 | 29 | typedef Validator = String? Function(String?); 30 | 31 | Function timeInputValidatorConstructor({minTimerTime = 1, maxTimerTime = 60}) { 32 | String? validator(String? input) { 33 | if (input != null && input != "") { 34 | var minutes = int.parse(input); 35 | if (minutes >= minTimerTime && minutes <= maxTimerTime) { 36 | return null; 37 | } 38 | } 39 | return "Interval time should be a number between $minTimerTime and $maxTimerTime."; 40 | } 41 | 42 | return validator; 43 | } 44 | 45 | // example of how you could extend Logger with a custom function that takes a tag parameter if you like 46 | // class NLogger extends Logger { 47 | // void nlog(String tag, dynamic message, 48 | // [Level level = Level.info, dynamic error, StackTrace? stackTrace]) { 49 | // this.log(level, "${tag.toUpperCase()}: $message", error: error, stackTrace: stackTrace); 50 | // } 51 | // } 52 | 53 | var log = Logger( 54 | filter: null, // Use the default LogFilter (-> only log in debug mode) 55 | printer: PrettyPrinter( 56 | // Use the PrettyPrinter to format and print log 57 | methodCount: 0, 58 | // number of method calls to be displayed 59 | errorMethodCount: 8, 60 | // number of method calls if stacktrace is provided 61 | lineLength: 120, 62 | // width of the output 63 | colors: true, 64 | // Colorful log messages 65 | printEmojis: false, 66 | // Print an emoji for each log message 67 | printTime: true // Should each log print contain a timestamp 68 | ), 69 | output: null, // Use the default LogOutput (-> send everything to console) 70 | ); 71 | 72 | List getDurationNumbers(Duration d) { 73 | var microseconds = d.inMicroseconds; 74 | var microsecondsPerHour = Duration.microsecondsPerHour; 75 | var microsecondsPerMinute = Duration.microsecondsPerMinute; 76 | var microsecondsPerSecond = Duration.microsecondsPerSecond; 77 | 78 | var hours = microseconds ~/ microsecondsPerHour; 79 | microseconds = microseconds.remainder(microsecondsPerHour); 80 | 81 | // if (microseconds < 0) microseconds = -microseconds; 82 | 83 | var minutes = microseconds ~/ microsecondsPerMinute; 84 | microseconds = microseconds.remainder(microsecondsPerMinute); 85 | 86 | // var minutesPadding = minutes < 10 ? "0" : ""; 87 | 88 | var seconds = microseconds ~/ microsecondsPerSecond; 89 | microseconds = microseconds.remainder(microsecondsPerSecond); 90 | 91 | // var secondsPadding = seconds < 10 ? "0" : ""; 92 | // 93 | // var paddedMicroseconds = microseconds.toString().padLeft(6, "0"); 94 | // return "$hours:" 95 | // "$minutesPadding$minutes:" 96 | // "$secondsPadding$seconds.$paddedMicroseconds"; 97 | 98 | return [hours, minutes, seconds]; 99 | } 100 | 101 | String formatSeconds(int seconds) { 102 | int mm = seconds ~/ 60; 103 | int ss = seconds % 60; 104 | return mm.toString().padLeft(2, '0') + ':' + ss.toString().padLeft(2, '0'); 105 | } 106 | 107 | Duration timeLeftTo(DateTime endTime, {bool roundToZero = false}) { 108 | // Duration timeLeft = endTime.difference(DateTime.now()) + Duration(seconds: 1); 109 | Duration timeLeft = endTime.difference(DateTime.now()); 110 | if (roundToZero && timeLeft.inMilliseconds.abs() < 1000) { 111 | log.d("rounding timeleft to zero because within 1s. was ${timeLeft.inMilliseconds} ms"); 112 | // if close enough to 0, round to 0 113 | timeLeft = Duration.zero; 114 | } 115 | return timeLeft; 116 | } 117 | 118 | String timeLeftString(Duration d) { 119 | if (!d.isNegative && d != Duration.zero) { 120 | // we have to add 1 second because otherwise the range 9.9 - 9.0 is all called 9 121 | // but it's more accurate to call that 10 and switch to 9 when reaching 9 122 | d += Duration(seconds: 1); 123 | } 124 | 125 | return formatDuration(d); 126 | } 127 | 128 | String formatDuration(Duration d) { 129 | var formattedString = d.toString().split('.').first; 130 | // remove leading - 131 | formattedString = formattedString.replaceFirst('-', ''); 132 | if (formattedString.startsWith('0')) { 133 | // if we have 0 hours, remove hours 134 | formattedString = formattedString.substring(2); 135 | } 136 | 137 | // readd leading - 138 | if (d.isNegative) { 139 | return '-$formattedString'; 140 | } else { 141 | return formattedString; 142 | } 143 | } 144 | 145 | Future requestBatteryOptimization(context) async { 146 | // checking and asking permissions for flutter_background 147 | if (await FlutterBackground.hasPermissions) { 148 | return; 149 | } 150 | 151 | await showDialog( 152 | context: context, 153 | barrierDismissible: false, 154 | builder: (_) => AlertDialog( 155 | title: Text("Ignore Battery Optimization"), 156 | // removing bottom padding from contentPadding. looks better. 157 | // contentPadding defaults: https://api.flutter.dev/flutter/material/AlertDialog/contentPadding.html 158 | contentPadding: EdgeInsets.fromLTRB(24, 20, 24, 0), 159 | content: SingleChildScrollView( 160 | child: Column( 161 | children: [ 162 | Text("On the next screen, please choose to allow the app to run in the background.\n"), 163 | Text( 164 | "This will allow the app to ignore Battery Optimization, and it's required to ensure that the timer can work reliably even when the app isn't focused or when the screen turns off.\n"), 165 | // Text("This feature will only be used when the timer is running."), 166 | ], 167 | ), 168 | ), 169 | actions: [ 170 | TextButton( 171 | child: Text("OK"), 172 | onPressed: () async { 173 | // this init call is just to ask for permission 174 | await initFlutterBackground(); 175 | Navigator.of(context).pop(); 176 | }, 177 | ), 178 | ], 179 | ), 180 | ); 181 | } 182 | 183 | Future<(bool, List)> requestNotificationPermissions(BuildContext context, 184 | { 185 | // if you only intends to request the permissions until app level, set the channelKey value to null 186 | required String? channelKey, 187 | required List permissionList}) async { 188 | bool requestedUserAction = false; 189 | 190 | // Check which of the permissions you need are allowed at this time 191 | List permissionsAllowed = await AwesomeNotifications() 192 | .checkPermissionList(channelKey: channelKey, permissions: permissionList); 193 | 194 | // If all permissions are allowed, there is nothing to do 195 | if (permissionsAllowed.length == permissionList.length) 196 | return (requestedUserAction, permissionsAllowed); 197 | 198 | // Refresh the permission list with only the disallowed permissions 199 | List permissionsNeeded = 200 | permissionList.toSet().difference(permissionsAllowed.toSet()).toList(); 201 | 202 | // Check if some of the permissions needed request user's intervention to be enabled 203 | List lockedPermissions = await AwesomeNotifications() 204 | .shouldShowRationaleToRequest(channelKey: channelKey, permissions: permissionsNeeded); 205 | 206 | // If there is no permissions depending on user's intervention, so request it directly 207 | if (lockedPermissions.isEmpty) { 208 | // Request the permission through native resources. 209 | await AwesomeNotifications().requestPermissionToSendNotifications( 210 | channelKey: channelKey, permissions: permissionsNeeded); 211 | 212 | // After the user come back, check if the permissions has successfully enabled 213 | permissionsAllowed = await AwesomeNotifications() 214 | .checkPermissionList(channelKey: channelKey, permissions: permissionsNeeded); 215 | } else { 216 | // If you need to show a rationale to educate the user to conceived the permission, show it 217 | if (channelKey != null) { 218 | // user has manually disabled or modified a channel, which makes this really hard to solve 219 | // you'd have to ask the user to disable the channel and reenable it 220 | // that seems to always set the needed permissions 221 | // but you can't really lock the app on an endless dialog if the user doesn't comply 222 | // you could at least detect if the channel was disabled and ask to re-enable 223 | // but you can't check for channel enabled status without also checking for a specific permission 224 | // so you could check for something like light, but you cannot check for something like alert 225 | // because alert can't seem to be activated manually 226 | // i'd say the best bet is to just not do anything 227 | // we also don't know how it works on every version of android 228 | log.e( 229 | "channel permissions have been tampered with. couldn't enable: ${lockedPermissions} on channel: ${channelKey}"); 230 | // ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Issue with notification settings. If app doesn't work, try uninstalling and reinstalling"))); 231 | // await showDialog( 232 | // context: context, 233 | // builder: (context) => AlertDialog( 234 | // title: const Text('Issue with notifications'), 235 | // content: const Text( 236 | // "There is a problem with the notification permissions for this app.\n\n" 237 | // "On the next screen, please disable and re-enabled the shown notification channel.\n\n" 238 | // "These are required for reliably notifying you of when the meditation session ends.\n\n"), 239 | // actions: [ 240 | // TextButton( 241 | // style: TextButton.styleFrom( 242 | // textStyle: Theme.of(context).textTheme.labelLarge, 243 | // ), 244 | // child: const Text('OK'), 245 | // onPressed: () async { 246 | // // Request the permission through native resources. Only one page redirection is done at this point. 247 | // await AwesomeNotifications().requestPermissionToSendNotifications( 248 | // channelKey: channelKey, permissions: lockedPermissions); 249 | // 250 | // // After the user come back, check if the permissions has successfully enabled 251 | // permissionsAllowed = await AwesomeNotifications().checkPermissionList( 252 | // channelKey: channelKey, permissions: lockedPermissions); 253 | // 254 | // Navigator.of(context).pop(); 255 | // }, 256 | // ), 257 | // ], 258 | // )); 259 | } else { 260 | requestedUserAction = true; 261 | await showDialog( 262 | context: context, 263 | builder: (context) => AlertDialog( 264 | title: const Text('Allow Notifications'), 265 | content: const Text( 266 | "On the next screen, please allow notifications for the app.\n\n" 267 | "These notifications are required for reliably notifying you of when the meditation session ends.\n\n"), 268 | actions: [ 269 | TextButton( 270 | style: TextButton.styleFrom( 271 | textStyle: Theme.of(context).textTheme.labelLarge, 272 | ), 273 | child: const Text('OK'), 274 | onPressed: () async { 275 | // Request the permission through native resources. Only one page redirection is done at this point. 276 | await AwesomeNotifications().requestPermissionToSendNotifications( 277 | channelKey: channelKey, permissions: lockedPermissions); 278 | 279 | // After the user come back, check if the permissions has successfully enabled 280 | permissionsAllowed = await AwesomeNotifications().checkPermissionList( 281 | channelKey: channelKey, permissions: lockedPermissions); 282 | 283 | Navigator.of(context).pop(); 284 | }, 285 | ), 286 | ], 287 | )); 288 | } 289 | } 290 | 291 | // Return the updated list of allowed permissions 292 | return (requestedUserAction, permissionsAllowed); 293 | } 294 | 295 | Future requestUserToEnableChannel(context, channelKey) async { 296 | await showDialog( 297 | context: context, 298 | builder: (context) => AlertDialog( 299 | title: const Text('Enable Notification Channel'), 300 | content: const Text("On the next screen, please enable the notification channel.\n\n" 301 | "These notifications are required for reliably notifying you of when the meditation session ends.\n\n"), 302 | actions: [ 303 | TextButton( 304 | style: TextButton.styleFrom( 305 | textStyle: Theme.of(context).textTheme.labelLarge, 306 | ), 307 | child: const Text('OK'), 308 | onPressed: () async { 309 | var helper = ChannelHelper(); 310 | bool isChannelEnabled = await helper.isNotificationChannelEnabled(channelKey); 311 | if (!isChannelEnabled) { 312 | helper.openNotificationChannelSettings(channelKey); 313 | } 314 | 315 | Navigator.of(context).pop(); 316 | }, 317 | ), 318 | ], 319 | )); 320 | } 321 | 322 | Future requestPermissionDND(context, DoNotDisturbPlugin dndPlugin, 323 | {bool suggestDisable = false}) async { 324 | bool hasPermissions = await dndPlugin.isNotificationPolicyAccessGranted(); 325 | if (!hasPermissions) { 326 | await showDialog( 327 | context: context, 328 | barrierDismissible: false, 329 | builder: (_) => AlertDialog( 330 | title: Text("Permission required"), 331 | // removing bottom padding from contentPadding. looks better. 332 | // contentPadding defaults: https://api.flutter.dev/flutter/material/AlertDialog/contentPadding.html 333 | contentPadding: EdgeInsets.fromLTRB(23, 20, 24, 0), 334 | content: SingleChildScrollView( 335 | child: Column( 336 | children: [ 337 | suggestDisable 338 | ? Text( 339 | "You enabled 'Do not disturb' mode in the app settings, but this app no longer has the required permission. Press OK to grant Do Not Disturb access, or Cancel to disable the feature.\n") 340 | : Text( 341 | "You'll now be taken to your system settings where you can grant this app the permission to access Do Not Disturb settings.\n"), 342 | ], 343 | ), 344 | ), 345 | actions: [ 346 | if (suggestDisable) 347 | TextButton( 348 | onPressed: () async { 349 | Navigator.of(context).pop(); 350 | await Settings.setValue('dnd', false); 351 | }, 352 | child: Text("Cancel")), 353 | TextButton( 354 | child: Text("OK"), 355 | onPressed: () async { 356 | Navigator.of(context).pop(); 357 | await dndPlugin.openNotificationPolicyAccessSettings(); 358 | }, 359 | ), 360 | ], 361 | ), 362 | ); 363 | } 364 | } 365 | 366 | class ChannelHelper { 367 | static const platform = MethodChannel('com.nyxkn.meditation/channelHelper'); 368 | 369 | Future isNotificationChannelEnabled(String channelId) async { 370 | try { 371 | final bool isEnabled = 372 | await platform.invokeMethod('isChannelEnabled', {'channelId': channelId}); 373 | return isEnabled; 374 | } on PlatformException catch (e) { 375 | print("Error checking notification channel status: ${e.message}"); 376 | return false; 377 | } 378 | } 379 | 380 | Future openNotificationChannelSettings(String channelId) async { 381 | try { 382 | await platform.invokeMethod('openChannelSettings', {'channelId': channelId}); 383 | } on PlatformException catch (e) { 384 | print("Failed to open channel settings: ${e.message}"); 385 | } 386 | } 387 | } 388 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | app_settings: 5 | dependency: "direct main" 6 | description: 7 | name: app_settings 8 | sha256: "09bc7fe0313a507087bec1a3baf555f0576e816a760cbb31813a88890a09d9e5" 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "5.1.1" 12 | archive: 13 | dependency: transitive 14 | description: 15 | name: archive 16 | sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "3.6.1" 20 | args: 21 | dependency: transitive 22 | description: 23 | name: args 24 | sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "2.6.0" 28 | async: 29 | dependency: transitive 30 | description: 31 | name: async 32 | sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" 33 | url: "https://pub.dev" 34 | source: hosted 35 | version: "2.11.0" 36 | audioplayers: 37 | dependency: "direct main" 38 | description: 39 | name: audioplayers 40 | sha256: c346ba5a39dc208f1bab55fc239855f573d69b0e832402114bf0b793622adc4d 41 | url: "https://pub.dev" 42 | source: hosted 43 | version: "6.1.0" 44 | audioplayers_android: 45 | dependency: transitive 46 | description: 47 | name: audioplayers_android 48 | sha256: de576b890befe27175c2f511ba8b742bec83765fa97c3ce4282bba46212f58e4 49 | url: "https://pub.dev" 50 | source: hosted 51 | version: "5.0.0" 52 | audioplayers_darwin: 53 | dependency: transitive 54 | description: 55 | name: audioplayers_darwin 56 | sha256: e507887f3ff18d8e5a10a668d7bedc28206b12e10b98347797257c6ae1019c3b 57 | url: "https://pub.dev" 58 | source: hosted 59 | version: "6.0.0" 60 | audioplayers_linux: 61 | dependency: transitive 62 | description: 63 | name: audioplayers_linux 64 | sha256: "3d3d244c90436115417f170426ce768856d8fe4dfc5ed66a049d2890acfa82f9" 65 | url: "https://pub.dev" 66 | source: hosted 67 | version: "4.0.0" 68 | audioplayers_platform_interface: 69 | dependency: transitive 70 | description: 71 | name: audioplayers_platform_interface 72 | sha256: "6834dd48dfb7bc6c2404998ebdd161f79cd3774a7e6779e1348d54a3bfdcfaa5" 73 | url: "https://pub.dev" 74 | source: hosted 75 | version: "7.0.0" 76 | audioplayers_web: 77 | dependency: transitive 78 | description: 79 | name: audioplayers_web 80 | sha256: "3609bdf0e05e66a3d9750ee40b1e37f2a622c4edb796cc600b53a90a30a2ace4" 81 | url: "https://pub.dev" 82 | source: hosted 83 | version: "5.0.1" 84 | audioplayers_windows: 85 | dependency: transitive 86 | description: 87 | name: audioplayers_windows 88 | sha256: "8605762dddba992138d476f6a0c3afd9df30ac5b96039929063eceed416795c2" 89 | url: "https://pub.dev" 90 | source: hosted 91 | version: "4.0.0" 92 | awesome_notifications: 93 | dependency: "direct main" 94 | description: 95 | name: awesome_notifications 96 | sha256: d051ffb694a53da216ff13d02c8ec645d75320048262f7e6b3c1d95a4f54c902 97 | url: "https://pub.dev" 98 | source: hosted 99 | version: "0.10.0" 100 | boolean_selector: 101 | dependency: transitive 102 | description: 103 | name: boolean_selector 104 | sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" 105 | url: "https://pub.dev" 106 | source: hosted 107 | version: "2.1.1" 108 | characters: 109 | dependency: transitive 110 | description: 111 | name: characters 112 | sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" 113 | url: "https://pub.dev" 114 | source: hosted 115 | version: "1.3.0" 116 | checked_yaml: 117 | dependency: transitive 118 | description: 119 | name: checked_yaml 120 | sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff 121 | url: "https://pub.dev" 122 | source: hosted 123 | version: "2.0.3" 124 | cli_util: 125 | dependency: transitive 126 | description: 127 | name: cli_util 128 | sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c 129 | url: "https://pub.dev" 130 | source: hosted 131 | version: "0.4.2" 132 | clock: 133 | dependency: transitive 134 | description: 135 | name: clock 136 | sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf 137 | url: "https://pub.dev" 138 | source: hosted 139 | version: "1.1.1" 140 | collection: 141 | dependency: transitive 142 | description: 143 | name: collection 144 | sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a 145 | url: "https://pub.dev" 146 | source: hosted 147 | version: "1.18.0" 148 | crypto: 149 | dependency: transitive 150 | description: 151 | name: crypto 152 | sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" 153 | url: "https://pub.dev" 154 | source: hosted 155 | version: "3.0.6" 156 | dbus: 157 | dependency: transitive 158 | description: 159 | name: dbus 160 | sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" 161 | url: "https://pub.dev" 162 | source: hosted 163 | version: "0.7.10" 164 | do_not_disturb: 165 | dependency: "direct main" 166 | description: 167 | name: do_not_disturb 168 | sha256: dc1cec3a7a8f41d57576b662f762f806f99709e1d711441f6a29b73902d838cd 169 | url: "https://pub.dev" 170 | source: hosted 171 | version: "1.0.2" 172 | event_bus: 173 | dependency: "direct main" 174 | description: 175 | name: event_bus 176 | sha256: "1a55e97923769c286d295240048fc180e7b0768902c3c2e869fe059aafa15304" 177 | url: "https://pub.dev" 178 | source: hosted 179 | version: "2.0.1" 180 | fake_async: 181 | dependency: transitive 182 | description: 183 | name: fake_async 184 | sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" 185 | url: "https://pub.dev" 186 | source: hosted 187 | version: "1.3.1" 188 | ffi: 189 | dependency: transitive 190 | description: 191 | name: ffi 192 | sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" 193 | url: "https://pub.dev" 194 | source: hosted 195 | version: "2.1.3" 196 | file: 197 | dependency: transitive 198 | description: 199 | name: file 200 | sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 201 | url: "https://pub.dev" 202 | source: hosted 203 | version: "7.0.1" 204 | fixnum: 205 | dependency: transitive 206 | description: 207 | name: fixnum 208 | sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be 209 | url: "https://pub.dev" 210 | source: hosted 211 | version: "1.1.1" 212 | flutter: 213 | dependency: "direct main" 214 | description: flutter 215 | source: sdk 216 | version: "0.0.0" 217 | flutter_background: 218 | dependency: "direct main" 219 | description: 220 | name: flutter_background 221 | sha256: "8dad66e3102da2b4046cc3adcf70625578809827170bd78e36e5890c074287d2" 222 | url: "https://pub.dev" 223 | source: hosted 224 | version: "1.3.0+1" 225 | flutter_launcher_icons: 226 | dependency: "direct dev" 227 | description: 228 | name: flutter_launcher_icons 229 | sha256: "619817c4b65b322b5104b6bb6dfe6cda62d9729bd7ad4303ecc8b4e690a67a77" 230 | url: "https://pub.dev" 231 | source: hosted 232 | version: "0.14.1" 233 | flutter_lints: 234 | dependency: "direct dev" 235 | description: 236 | name: flutter_lints 237 | sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" 238 | url: "https://pub.dev" 239 | source: hosted 240 | version: "5.0.0" 241 | flutter_settings_screens: 242 | dependency: "direct main" 243 | description: 244 | path: "." 245 | ref: "18f9377" 246 | resolved-ref: "18f937739d7fb4c7ceb0d8b9877f20f0b5e9e6d8" 247 | url: "https://github.com/GAM3RG33K/flutter_settings_screens.git" 248 | source: git 249 | version: "0.3.4" 250 | flutter_test: 251 | dependency: "direct dev" 252 | description: flutter 253 | source: sdk 254 | version: "0.0.0" 255 | flutter_web_plugins: 256 | dependency: transitive 257 | description: flutter 258 | source: sdk 259 | version: "0.0.0" 260 | get_it: 261 | dependency: "direct main" 262 | description: 263 | name: get_it 264 | sha256: c49895c1ecb0ee2a0ec568d39de882e2c299ba26355aa6744ab1001f98cebd15 265 | url: "https://pub.dev" 266 | source: hosted 267 | version: "8.0.2" 268 | http: 269 | dependency: transitive 270 | description: 271 | name: http 272 | sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 273 | url: "https://pub.dev" 274 | source: hosted 275 | version: "1.2.2" 276 | http_parser: 277 | dependency: transitive 278 | description: 279 | name: http_parser 280 | sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" 281 | url: "https://pub.dev" 282 | source: hosted 283 | version: "4.0.2" 284 | image: 285 | dependency: transitive 286 | description: 287 | name: image 288 | sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d 289 | url: "https://pub.dev" 290 | source: hosted 291 | version: "4.3.0" 292 | intl: 293 | dependency: transitive 294 | description: 295 | name: intl 296 | sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf 297 | url: "https://pub.dev" 298 | source: hosted 299 | version: "0.19.0" 300 | json_annotation: 301 | dependency: transitive 302 | description: 303 | name: json_annotation 304 | sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" 305 | url: "https://pub.dev" 306 | source: hosted 307 | version: "4.9.0" 308 | leak_tracker: 309 | dependency: transitive 310 | description: 311 | name: leak_tracker 312 | sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" 313 | url: "https://pub.dev" 314 | source: hosted 315 | version: "10.0.5" 316 | leak_tracker_flutter_testing: 317 | dependency: transitive 318 | description: 319 | name: leak_tracker_flutter_testing 320 | sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" 321 | url: "https://pub.dev" 322 | source: hosted 323 | version: "3.0.5" 324 | leak_tracker_testing: 325 | dependency: transitive 326 | description: 327 | name: leak_tracker_testing 328 | sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" 329 | url: "https://pub.dev" 330 | source: hosted 331 | version: "3.0.1" 332 | lints: 333 | dependency: transitive 334 | description: 335 | name: lints 336 | sha256: "3315600f3fb3b135be672bf4a178c55f274bebe368325ae18462c89ac1e3b413" 337 | url: "https://pub.dev" 338 | source: hosted 339 | version: "5.0.0" 340 | logger: 341 | dependency: "direct main" 342 | description: 343 | name: logger 344 | sha256: be4b23575aac7ebf01f225a241eb7f6b5641eeaf43c6a8613510fc2f8cf187d1 345 | url: "https://pub.dev" 346 | source: hosted 347 | version: "2.5.0" 348 | matcher: 349 | dependency: transitive 350 | description: 351 | name: matcher 352 | sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb 353 | url: "https://pub.dev" 354 | source: hosted 355 | version: "0.12.16+1" 356 | material_color_utilities: 357 | dependency: transitive 358 | description: 359 | name: material_color_utilities 360 | sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec 361 | url: "https://pub.dev" 362 | source: hosted 363 | version: "0.11.1" 364 | meta: 365 | dependency: transitive 366 | description: 367 | name: meta 368 | sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 369 | url: "https://pub.dev" 370 | source: hosted 371 | version: "1.15.0" 372 | nested: 373 | dependency: transitive 374 | description: 375 | name: nested 376 | sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" 377 | url: "https://pub.dev" 378 | source: hosted 379 | version: "1.0.0" 380 | package_info_plus: 381 | dependency: "direct main" 382 | description: 383 | name: package_info_plus 384 | sha256: da8d9ac8c4b1df253d1a328b7bf01ae77ef132833479ab40763334db13b91cce 385 | url: "https://pub.dev" 386 | source: hosted 387 | version: "8.1.1" 388 | package_info_plus_platform_interface: 389 | dependency: transitive 390 | description: 391 | name: package_info_plus_platform_interface 392 | sha256: ac1f4a4847f1ade8e6a87d1f39f5d7c67490738642e2542f559ec38c37489a66 393 | url: "https://pub.dev" 394 | source: hosted 395 | version: "3.0.1" 396 | path: 397 | dependency: transitive 398 | description: 399 | name: path 400 | sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" 401 | url: "https://pub.dev" 402 | source: hosted 403 | version: "1.9.0" 404 | path_provider: 405 | dependency: transitive 406 | description: 407 | name: path_provider 408 | sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" 409 | url: "https://pub.dev" 410 | source: hosted 411 | version: "2.1.5" 412 | path_provider_android: 413 | dependency: transitive 414 | description: 415 | name: path_provider_android 416 | sha256: "8c4967f8b7cb46dc914e178daa29813d83ae502e0529d7b0478330616a691ef7" 417 | url: "https://pub.dev" 418 | source: hosted 419 | version: "2.2.14" 420 | path_provider_foundation: 421 | dependency: transitive 422 | description: 423 | name: path_provider_foundation 424 | sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 425 | url: "https://pub.dev" 426 | source: hosted 427 | version: "2.4.0" 428 | path_provider_linux: 429 | dependency: transitive 430 | description: 431 | name: path_provider_linux 432 | sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 433 | url: "https://pub.dev" 434 | source: hosted 435 | version: "2.2.1" 436 | path_provider_platform_interface: 437 | dependency: transitive 438 | description: 439 | name: path_provider_platform_interface 440 | sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" 441 | url: "https://pub.dev" 442 | source: hosted 443 | version: "2.1.2" 444 | path_provider_windows: 445 | dependency: transitive 446 | description: 447 | name: path_provider_windows 448 | sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 449 | url: "https://pub.dev" 450 | source: hosted 451 | version: "2.3.0" 452 | petitparser: 453 | dependency: transitive 454 | description: 455 | name: petitparser 456 | sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 457 | url: "https://pub.dev" 458 | source: hosted 459 | version: "6.0.2" 460 | platform: 461 | dependency: transitive 462 | description: 463 | name: platform 464 | sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" 465 | url: "https://pub.dev" 466 | source: hosted 467 | version: "3.1.6" 468 | plugin_platform_interface: 469 | dependency: transitive 470 | description: 471 | name: plugin_platform_interface 472 | sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" 473 | url: "https://pub.dev" 474 | source: hosted 475 | version: "2.1.8" 476 | provider: 477 | dependency: transitive 478 | description: 479 | name: provider 480 | sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c 481 | url: "https://pub.dev" 482 | source: hosted 483 | version: "6.1.2" 484 | shared_preferences: 485 | dependency: "direct main" 486 | description: 487 | name: shared_preferences 488 | sha256: "95f9997ca1fb9799d494d0cb2a780fd7be075818d59f00c43832ed112b158a82" 489 | url: "https://pub.dev" 490 | source: hosted 491 | version: "2.3.3" 492 | shared_preferences_android: 493 | dependency: transitive 494 | description: 495 | name: shared_preferences_android 496 | sha256: "3b9febd815c9ca29c9e3520d50ec32f49157711e143b7a4ca039eb87e8ade5ab" 497 | url: "https://pub.dev" 498 | source: hosted 499 | version: "2.3.3" 500 | shared_preferences_foundation: 501 | dependency: transitive 502 | description: 503 | name: shared_preferences_foundation 504 | sha256: "07e050c7cd39bad516f8d64c455f04508d09df104be326d8c02551590a0d513d" 505 | url: "https://pub.dev" 506 | source: hosted 507 | version: "2.5.3" 508 | shared_preferences_linux: 509 | dependency: transitive 510 | description: 511 | name: shared_preferences_linux 512 | sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" 513 | url: "https://pub.dev" 514 | source: hosted 515 | version: "2.4.1" 516 | shared_preferences_platform_interface: 517 | dependency: transitive 518 | description: 519 | name: shared_preferences_platform_interface 520 | sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" 521 | url: "https://pub.dev" 522 | source: hosted 523 | version: "2.4.1" 524 | shared_preferences_web: 525 | dependency: transitive 526 | description: 527 | name: shared_preferences_web 528 | sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e 529 | url: "https://pub.dev" 530 | source: hosted 531 | version: "2.4.2" 532 | shared_preferences_windows: 533 | dependency: transitive 534 | description: 535 | name: shared_preferences_windows 536 | sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" 537 | url: "https://pub.dev" 538 | source: hosted 539 | version: "2.4.1" 540 | sky_engine: 541 | dependency: transitive 542 | description: flutter 543 | source: sdk 544 | version: "0.0.99" 545 | source_span: 546 | dependency: transitive 547 | description: 548 | name: source_span 549 | sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" 550 | url: "https://pub.dev" 551 | source: hosted 552 | version: "1.10.0" 553 | sprintf: 554 | dependency: transitive 555 | description: 556 | name: sprintf 557 | sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" 558 | url: "https://pub.dev" 559 | source: hosted 560 | version: "7.0.0" 561 | stack_trace: 562 | dependency: transitive 563 | description: 564 | name: stack_trace 565 | sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" 566 | url: "https://pub.dev" 567 | source: hosted 568 | version: "1.11.1" 569 | stream_channel: 570 | dependency: transitive 571 | description: 572 | name: stream_channel 573 | sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 574 | url: "https://pub.dev" 575 | source: hosted 576 | version: "2.1.2" 577 | string_scanner: 578 | dependency: transitive 579 | description: 580 | name: string_scanner 581 | sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" 582 | url: "https://pub.dev" 583 | source: hosted 584 | version: "1.2.0" 585 | synchronized: 586 | dependency: transitive 587 | description: 588 | name: synchronized 589 | sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225" 590 | url: "https://pub.dev" 591 | source: hosted 592 | version: "3.3.0+3" 593 | term_glyph: 594 | dependency: transitive 595 | description: 596 | name: term_glyph 597 | sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 598 | url: "https://pub.dev" 599 | source: hosted 600 | version: "1.2.1" 601 | test_api: 602 | dependency: transitive 603 | description: 604 | name: test_api 605 | sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" 606 | url: "https://pub.dev" 607 | source: hosted 608 | version: "0.7.2" 609 | typed_data: 610 | dependency: transitive 611 | description: 612 | name: typed_data 613 | sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 614 | url: "https://pub.dev" 615 | source: hosted 616 | version: "1.4.0" 617 | uuid: 618 | dependency: transitive 619 | description: 620 | name: uuid 621 | sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff 622 | url: "https://pub.dev" 623 | source: hosted 624 | version: "4.5.1" 625 | vector_math: 626 | dependency: transitive 627 | description: 628 | name: vector_math 629 | sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" 630 | url: "https://pub.dev" 631 | source: hosted 632 | version: "2.1.4" 633 | vm_service: 634 | dependency: transitive 635 | description: 636 | name: vm_service 637 | sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" 638 | url: "https://pub.dev" 639 | source: hosted 640 | version: "14.2.5" 641 | volume_controller: 642 | dependency: "direct main" 643 | description: 644 | name: volume_controller 645 | sha256: c71d4c62631305df63b72da79089e078af2659649301807fa746088f365cb48e 646 | url: "https://pub.dev" 647 | source: hosted 648 | version: "2.0.8" 649 | wakelock_plus: 650 | dependency: "direct main" 651 | description: 652 | name: wakelock_plus 653 | sha256: bf4ee6f17a2fa373ed3753ad0e602b7603f8c75af006d5b9bdade263928c0484 654 | url: "https://pub.dev" 655 | source: hosted 656 | version: "1.2.8" 657 | wakelock_plus_platform_interface: 658 | dependency: transitive 659 | description: 660 | name: wakelock_plus_platform_interface 661 | sha256: "422d1cdbb448079a8a62a5a770b69baa489f8f7ca21aef47800c726d404f9d16" 662 | url: "https://pub.dev" 663 | source: hosted 664 | version: "1.2.1" 665 | web: 666 | dependency: transitive 667 | description: 668 | name: web 669 | sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb 670 | url: "https://pub.dev" 671 | source: hosted 672 | version: "1.1.0" 673 | win32: 674 | dependency: transitive 675 | description: 676 | name: win32 677 | sha256: "8b338d4486ab3fbc0ba0db9f9b4f5239b6697fcee427939a40e720cbb9ee0a69" 678 | url: "https://pub.dev" 679 | source: hosted 680 | version: "5.9.0" 681 | xdg_directories: 682 | dependency: transitive 683 | description: 684 | name: xdg_directories 685 | sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" 686 | url: "https://pub.dev" 687 | source: hosted 688 | version: "1.1.0" 689 | xml: 690 | dependency: transitive 691 | description: 692 | name: xml 693 | sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 694 | url: "https://pub.dev" 695 | source: hosted 696 | version: "6.5.0" 697 | yaml: 698 | dependency: transitive 699 | description: 700 | name: yaml 701 | sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" 702 | url: "https://pub.dev" 703 | source: hosted 704 | version: "3.1.2" 705 | sdks: 706 | dart: ">=3.5.3 <4.0.0" 707 | flutter: ">=3.24.0" 708 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 11 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 12 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 13 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 14 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 15 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 16 | C791E26E45EEF20C410E7F8B /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14C4FD3D29C01D2D6C87E73B /* Pods_Runner.framework */; }; 17 | /* End PBXBuildFile section */ 18 | 19 | /* Begin PBXCopyFilesBuildPhase section */ 20 | 9705A1C41CF9048500538489 /* Embed Frameworks */ = { 21 | isa = PBXCopyFilesBuildPhase; 22 | buildActionMask = 2147483647; 23 | dstPath = ""; 24 | dstSubfolderSpec = 10; 25 | files = ( 26 | ); 27 | name = "Embed Frameworks"; 28 | runOnlyForDeploymentPostprocessing = 0; 29 | }; 30 | /* End PBXCopyFilesBuildPhase section */ 31 | 32 | /* Begin PBXFileReference section */ 33 | 13E7143AB28314061BFBF918 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 34 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 35 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 36 | 14C4FD3D29C01D2D6C87E73B /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 37 | 37B905CEC831FFEC6D466408 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 38 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 39 | 453593492E696946D372A110 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 40 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 41 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 42 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 43 | 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 44 | 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 45 | 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 46 | 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 47 | 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 48 | 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 49 | 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 50 | /* End PBXFileReference section */ 51 | 52 | /* Begin PBXFrameworksBuildPhase section */ 53 | 97C146EB1CF9000F007C117D /* Frameworks */ = { 54 | isa = PBXFrameworksBuildPhase; 55 | buildActionMask = 2147483647; 56 | files = ( 57 | C791E26E45EEF20C410E7F8B /* Pods_Runner.framework in Frameworks */, 58 | ); 59 | runOnlyForDeploymentPostprocessing = 0; 60 | }; 61 | /* End PBXFrameworksBuildPhase section */ 62 | 63 | /* Begin PBXGroup section */ 64 | 25C39E8D4994D4785041DF5F /* Pods */ = { 65 | isa = PBXGroup; 66 | children = ( 67 | 13E7143AB28314061BFBF918 /* Pods-Runner.debug.xcconfig */, 68 | 453593492E696946D372A110 /* Pods-Runner.release.xcconfig */, 69 | 37B905CEC831FFEC6D466408 /* Pods-Runner.profile.xcconfig */, 70 | ); 71 | name = Pods; 72 | path = Pods; 73 | sourceTree = ""; 74 | }; 75 | 80DAA15CFB3D2244C6A0C625 /* Frameworks */ = { 76 | isa = PBXGroup; 77 | children = ( 78 | 14C4FD3D29C01D2D6C87E73B /* Pods_Runner.framework */, 79 | ); 80 | name = Frameworks; 81 | sourceTree = ""; 82 | }; 83 | 9740EEB11CF90186004384FC /* Flutter */ = { 84 | isa = PBXGroup; 85 | children = ( 86 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 87 | 9740EEB21CF90195004384FC /* Debug.xcconfig */, 88 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 89 | 9740EEB31CF90195004384FC /* Generated.xcconfig */, 90 | ); 91 | name = Flutter; 92 | sourceTree = ""; 93 | }; 94 | 97C146E51CF9000F007C117D = { 95 | isa = PBXGroup; 96 | children = ( 97 | 9740EEB11CF90186004384FC /* Flutter */, 98 | 97C146F01CF9000F007C117D /* Runner */, 99 | 97C146EF1CF9000F007C117D /* Products */, 100 | 25C39E8D4994D4785041DF5F /* Pods */, 101 | 80DAA15CFB3D2244C6A0C625 /* Frameworks */, 102 | ); 103 | sourceTree = ""; 104 | }; 105 | 97C146EF1CF9000F007C117D /* Products */ = { 106 | isa = PBXGroup; 107 | children = ( 108 | 97C146EE1CF9000F007C117D /* Runner.app */, 109 | ); 110 | name = Products; 111 | sourceTree = ""; 112 | }; 113 | 97C146F01CF9000F007C117D /* Runner */ = { 114 | isa = PBXGroup; 115 | children = ( 116 | 97C146FA1CF9000F007C117D /* Main.storyboard */, 117 | 97C146FD1CF9000F007C117D /* Assets.xcassets */, 118 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 119 | 97C147021CF9000F007C117D /* Info.plist */, 120 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 121 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 122 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 123 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, 124 | ); 125 | path = Runner; 126 | sourceTree = ""; 127 | }; 128 | /* End PBXGroup section */ 129 | 130 | /* Begin PBXNativeTarget section */ 131 | 97C146ED1CF9000F007C117D /* Runner */ = { 132 | isa = PBXNativeTarget; 133 | buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; 134 | buildPhases = ( 135 | ADAFAB38FB27149A65B5A8E3 /* [CP] Check Pods Manifest.lock */, 136 | 9740EEB61CF901F6004384FC /* Run Script */, 137 | 97C146EA1CF9000F007C117D /* Sources */, 138 | 97C146EB1CF9000F007C117D /* Frameworks */, 139 | 97C146EC1CF9000F007C117D /* Resources */, 140 | 9705A1C41CF9048500538489 /* Embed Frameworks */, 141 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 142 | 64A814248AEF8407D3CFA322 /* [CP] Embed Pods Frameworks */, 143 | ); 144 | buildRules = ( 145 | ); 146 | dependencies = ( 147 | ); 148 | name = Runner; 149 | productName = Runner; 150 | productReference = 97C146EE1CF9000F007C117D /* Runner.app */; 151 | productType = "com.apple.product-type.application"; 152 | }; 153 | /* End PBXNativeTarget section */ 154 | 155 | /* Begin PBXProject section */ 156 | 97C146E61CF9000F007C117D /* Project object */ = { 157 | isa = PBXProject; 158 | attributes = { 159 | LastUpgradeCheck = 1300; 160 | ORGANIZATIONNAME = ""; 161 | TargetAttributes = { 162 | 97C146ED1CF9000F007C117D = { 163 | CreatedOnToolsVersion = 7.3.1; 164 | LastSwiftMigration = 1100; 165 | }; 166 | }; 167 | }; 168 | buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; 169 | compatibilityVersion = "Xcode 9.3"; 170 | developmentRegion = en; 171 | hasScannedForEncodings = 0; 172 | knownRegions = ( 173 | en, 174 | Base, 175 | ); 176 | mainGroup = 97C146E51CF9000F007C117D; 177 | productRefGroup = 97C146EF1CF9000F007C117D /* Products */; 178 | projectDirPath = ""; 179 | projectRoot = ""; 180 | targets = ( 181 | 97C146ED1CF9000F007C117D /* Runner */, 182 | ); 183 | }; 184 | /* End PBXProject section */ 185 | 186 | /* Begin PBXResourcesBuildPhase section */ 187 | 97C146EC1CF9000F007C117D /* Resources */ = { 188 | isa = PBXResourcesBuildPhase; 189 | buildActionMask = 2147483647; 190 | files = ( 191 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 192 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 193 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 194 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 195 | ); 196 | runOnlyForDeploymentPostprocessing = 0; 197 | }; 198 | /* End PBXResourcesBuildPhase section */ 199 | 200 | /* Begin PBXShellScriptBuildPhase section */ 201 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 202 | isa = PBXShellScriptBuildPhase; 203 | buildActionMask = 2147483647; 204 | files = ( 205 | ); 206 | inputPaths = ( 207 | ); 208 | name = "Thin Binary"; 209 | outputPaths = ( 210 | ); 211 | runOnlyForDeploymentPostprocessing = 0; 212 | shellPath = /bin/sh; 213 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; 214 | }; 215 | 64A814248AEF8407D3CFA322 /* [CP] Embed Pods Frameworks */ = { 216 | isa = PBXShellScriptBuildPhase; 217 | buildActionMask = 2147483647; 218 | files = ( 219 | ); 220 | inputFileListPaths = ( 221 | "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", 222 | ); 223 | name = "[CP] Embed Pods Frameworks"; 224 | outputFileListPaths = ( 225 | "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", 226 | ); 227 | runOnlyForDeploymentPostprocessing = 0; 228 | shellPath = /bin/sh; 229 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; 230 | showEnvVarsInLog = 0; 231 | }; 232 | 9740EEB61CF901F6004384FC /* Run Script */ = { 233 | isa = PBXShellScriptBuildPhase; 234 | buildActionMask = 2147483647; 235 | files = ( 236 | ); 237 | inputPaths = ( 238 | ); 239 | name = "Run Script"; 240 | outputPaths = ( 241 | ); 242 | runOnlyForDeploymentPostprocessing = 0; 243 | shellPath = /bin/sh; 244 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; 245 | }; 246 | ADAFAB38FB27149A65B5A8E3 /* [CP] Check Pods Manifest.lock */ = { 247 | isa = PBXShellScriptBuildPhase; 248 | buildActionMask = 2147483647; 249 | files = ( 250 | ); 251 | inputFileListPaths = ( 252 | ); 253 | inputPaths = ( 254 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 255 | "${PODS_ROOT}/Manifest.lock", 256 | ); 257 | name = "[CP] Check Pods Manifest.lock"; 258 | outputFileListPaths = ( 259 | ); 260 | outputPaths = ( 261 | "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", 262 | ); 263 | runOnlyForDeploymentPostprocessing = 0; 264 | shellPath = /bin/sh; 265 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 266 | showEnvVarsInLog = 0; 267 | }; 268 | /* End PBXShellScriptBuildPhase section */ 269 | 270 | /* Begin PBXSourcesBuildPhase section */ 271 | 97C146EA1CF9000F007C117D /* Sources */ = { 272 | isa = PBXSourcesBuildPhase; 273 | buildActionMask = 2147483647; 274 | files = ( 275 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 276 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 277 | ); 278 | runOnlyForDeploymentPostprocessing = 0; 279 | }; 280 | /* End PBXSourcesBuildPhase section */ 281 | 282 | /* Begin PBXVariantGroup section */ 283 | 97C146FA1CF9000F007C117D /* Main.storyboard */ = { 284 | isa = PBXVariantGroup; 285 | children = ( 286 | 97C146FB1CF9000F007C117D /* Base */, 287 | ); 288 | name = Main.storyboard; 289 | sourceTree = ""; 290 | }; 291 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { 292 | isa = PBXVariantGroup; 293 | children = ( 294 | 97C147001CF9000F007C117D /* Base */, 295 | ); 296 | name = LaunchScreen.storyboard; 297 | sourceTree = ""; 298 | }; 299 | /* End PBXVariantGroup section */ 300 | 301 | /* Begin XCBuildConfiguration section */ 302 | 249021D3217E4FDB00AE95B9 /* Profile */ = { 303 | isa = XCBuildConfiguration; 304 | buildSettings = { 305 | ALWAYS_SEARCH_USER_PATHS = NO; 306 | CLANG_ANALYZER_NONNULL = YES; 307 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 308 | CLANG_CXX_LIBRARY = "libc++"; 309 | CLANG_ENABLE_MODULES = YES; 310 | CLANG_ENABLE_OBJC_ARC = YES; 311 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 312 | CLANG_WARN_BOOL_CONVERSION = YES; 313 | CLANG_WARN_COMMA = YES; 314 | CLANG_WARN_CONSTANT_CONVERSION = YES; 315 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 316 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 317 | CLANG_WARN_EMPTY_BODY = YES; 318 | CLANG_WARN_ENUM_CONVERSION = YES; 319 | CLANG_WARN_INFINITE_RECURSION = YES; 320 | CLANG_WARN_INT_CONVERSION = YES; 321 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 322 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 323 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 324 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 325 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 326 | CLANG_WARN_STRICT_PROTOTYPES = YES; 327 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 328 | CLANG_WARN_UNREACHABLE_CODE = YES; 329 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 330 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 331 | COPY_PHASE_STRIP = NO; 332 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 333 | ENABLE_NS_ASSERTIONS = NO; 334 | ENABLE_STRICT_OBJC_MSGSEND = YES; 335 | GCC_C_LANGUAGE_STANDARD = gnu99; 336 | GCC_NO_COMMON_BLOCKS = YES; 337 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 338 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 339 | GCC_WARN_UNDECLARED_SELECTOR = YES; 340 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 341 | GCC_WARN_UNUSED_FUNCTION = YES; 342 | GCC_WARN_UNUSED_VARIABLE = YES; 343 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 344 | MTL_ENABLE_DEBUG_INFO = NO; 345 | SDKROOT = iphoneos; 346 | SUPPORTED_PLATFORMS = iphoneos; 347 | TARGETED_DEVICE_FAMILY = "1,2"; 348 | VALIDATE_PRODUCT = YES; 349 | }; 350 | name = Profile; 351 | }; 352 | 249021D4217E4FDB00AE95B9 /* Profile */ = { 353 | isa = XCBuildConfiguration; 354 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 355 | buildSettings = { 356 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 357 | CLANG_ENABLE_MODULES = YES; 358 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 359 | DEVELOPMENT_TEAM = F9UBK6A8JZ; 360 | ENABLE_BITCODE = NO; 361 | INFOPLIST_FILE = Runner/Info.plist; 362 | LD_RUNPATH_SEARCH_PATHS = ( 363 | "$(inherited)", 364 | "@executable_path/Frameworks", 365 | ); 366 | PRODUCT_BUNDLE_IDENTIFIER = com.nyxkn.meditation; 367 | PRODUCT_NAME = "$(TARGET_NAME)"; 368 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 369 | SWIFT_VERSION = 5.0; 370 | VERSIONING_SYSTEM = "apple-generic"; 371 | }; 372 | name = Profile; 373 | }; 374 | 97C147031CF9000F007C117D /* Debug */ = { 375 | isa = XCBuildConfiguration; 376 | buildSettings = { 377 | ALWAYS_SEARCH_USER_PATHS = NO; 378 | CLANG_ANALYZER_NONNULL = YES; 379 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 380 | CLANG_CXX_LIBRARY = "libc++"; 381 | CLANG_ENABLE_MODULES = YES; 382 | CLANG_ENABLE_OBJC_ARC = YES; 383 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 384 | CLANG_WARN_BOOL_CONVERSION = YES; 385 | CLANG_WARN_COMMA = YES; 386 | CLANG_WARN_CONSTANT_CONVERSION = YES; 387 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 388 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 389 | CLANG_WARN_EMPTY_BODY = YES; 390 | CLANG_WARN_ENUM_CONVERSION = YES; 391 | CLANG_WARN_INFINITE_RECURSION = YES; 392 | CLANG_WARN_INT_CONVERSION = YES; 393 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 394 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 395 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 396 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 397 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 398 | CLANG_WARN_STRICT_PROTOTYPES = YES; 399 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 400 | CLANG_WARN_UNREACHABLE_CODE = YES; 401 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 402 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 403 | COPY_PHASE_STRIP = NO; 404 | DEBUG_INFORMATION_FORMAT = dwarf; 405 | ENABLE_STRICT_OBJC_MSGSEND = YES; 406 | ENABLE_TESTABILITY = YES; 407 | GCC_C_LANGUAGE_STANDARD = gnu99; 408 | GCC_DYNAMIC_NO_PIC = NO; 409 | GCC_NO_COMMON_BLOCKS = YES; 410 | GCC_OPTIMIZATION_LEVEL = 0; 411 | GCC_PREPROCESSOR_DEFINITIONS = ( 412 | "DEBUG=1", 413 | "$(inherited)", 414 | ); 415 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 416 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 417 | GCC_WARN_UNDECLARED_SELECTOR = YES; 418 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 419 | GCC_WARN_UNUSED_FUNCTION = YES; 420 | GCC_WARN_UNUSED_VARIABLE = YES; 421 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 422 | MTL_ENABLE_DEBUG_INFO = YES; 423 | ONLY_ACTIVE_ARCH = YES; 424 | SDKROOT = iphoneos; 425 | TARGETED_DEVICE_FAMILY = "1,2"; 426 | }; 427 | name = Debug; 428 | }; 429 | 97C147041CF9000F007C117D /* Release */ = { 430 | isa = XCBuildConfiguration; 431 | buildSettings = { 432 | ALWAYS_SEARCH_USER_PATHS = NO; 433 | CLANG_ANALYZER_NONNULL = YES; 434 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 435 | CLANG_CXX_LIBRARY = "libc++"; 436 | CLANG_ENABLE_MODULES = YES; 437 | CLANG_ENABLE_OBJC_ARC = YES; 438 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 439 | CLANG_WARN_BOOL_CONVERSION = YES; 440 | CLANG_WARN_COMMA = YES; 441 | CLANG_WARN_CONSTANT_CONVERSION = YES; 442 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 443 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 444 | CLANG_WARN_EMPTY_BODY = YES; 445 | CLANG_WARN_ENUM_CONVERSION = YES; 446 | CLANG_WARN_INFINITE_RECURSION = YES; 447 | CLANG_WARN_INT_CONVERSION = YES; 448 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 449 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 450 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 451 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 452 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 453 | CLANG_WARN_STRICT_PROTOTYPES = YES; 454 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 455 | CLANG_WARN_UNREACHABLE_CODE = YES; 456 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 457 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 458 | COPY_PHASE_STRIP = NO; 459 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 460 | ENABLE_NS_ASSERTIONS = NO; 461 | ENABLE_STRICT_OBJC_MSGSEND = YES; 462 | GCC_C_LANGUAGE_STANDARD = gnu99; 463 | GCC_NO_COMMON_BLOCKS = YES; 464 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 465 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 466 | GCC_WARN_UNDECLARED_SELECTOR = YES; 467 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 468 | GCC_WARN_UNUSED_FUNCTION = YES; 469 | GCC_WARN_UNUSED_VARIABLE = YES; 470 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 471 | MTL_ENABLE_DEBUG_INFO = NO; 472 | SDKROOT = iphoneos; 473 | SUPPORTED_PLATFORMS = iphoneos; 474 | SWIFT_COMPILATION_MODE = wholemodule; 475 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 476 | TARGETED_DEVICE_FAMILY = "1,2"; 477 | VALIDATE_PRODUCT = YES; 478 | }; 479 | name = Release; 480 | }; 481 | 97C147061CF9000F007C117D /* Debug */ = { 482 | isa = XCBuildConfiguration; 483 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 484 | buildSettings = { 485 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 486 | CLANG_ENABLE_MODULES = YES; 487 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 488 | DEVELOPMENT_TEAM = F9UBK6A8JZ; 489 | ENABLE_BITCODE = NO; 490 | INFOPLIST_FILE = Runner/Info.plist; 491 | LD_RUNPATH_SEARCH_PATHS = ( 492 | "$(inherited)", 493 | "@executable_path/Frameworks", 494 | ); 495 | PRODUCT_BUNDLE_IDENTIFIER = com.nyxkn.meditation; 496 | PRODUCT_NAME = "$(TARGET_NAME)"; 497 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 498 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 499 | SWIFT_VERSION = 5.0; 500 | VERSIONING_SYSTEM = "apple-generic"; 501 | }; 502 | name = Debug; 503 | }; 504 | 97C147071CF9000F007C117D /* Release */ = { 505 | isa = XCBuildConfiguration; 506 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 507 | buildSettings = { 508 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 509 | CLANG_ENABLE_MODULES = YES; 510 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 511 | DEVELOPMENT_TEAM = F9UBK6A8JZ; 512 | ENABLE_BITCODE = NO; 513 | INFOPLIST_FILE = Runner/Info.plist; 514 | LD_RUNPATH_SEARCH_PATHS = ( 515 | "$(inherited)", 516 | "@executable_path/Frameworks", 517 | ); 518 | PRODUCT_BUNDLE_IDENTIFIER = com.nyxkn.meditation; 519 | PRODUCT_NAME = "$(TARGET_NAME)"; 520 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 521 | SWIFT_VERSION = 5.0; 522 | VERSIONING_SYSTEM = "apple-generic"; 523 | }; 524 | name = Release; 525 | }; 526 | /* End XCBuildConfiguration section */ 527 | 528 | /* Begin XCConfigurationList section */ 529 | 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { 530 | isa = XCConfigurationList; 531 | buildConfigurations = ( 532 | 97C147031CF9000F007C117D /* Debug */, 533 | 97C147041CF9000F007C117D /* Release */, 534 | 249021D3217E4FDB00AE95B9 /* Profile */, 535 | ); 536 | defaultConfigurationIsVisible = 0; 537 | defaultConfigurationName = Release; 538 | }; 539 | 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { 540 | isa = XCConfigurationList; 541 | buildConfigurations = ( 542 | 97C147061CF9000F007C117D /* Debug */, 543 | 97C147071CF9000F007C117D /* Release */, 544 | 249021D4217E4FDB00AE95B9 /* Profile */, 545 | ); 546 | defaultConfigurationIsVisible = 0; 547 | defaultConfigurationName = Release; 548 | }; 549 | /* End XCConfigurationList section */ 550 | }; 551 | rootObject = 97C146E61CF9000F007C117D /* Project object */; 552 | } 553 | -------------------------------------------------------------------------------- /lib/timer.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:flutter/foundation.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter/scheduler.dart' hide Priority; 7 | 8 | import 'package:awesome_notifications/awesome_notifications.dart'; 9 | import 'package:flutter/services.dart'; 10 | import 'package:flutter_background/flutter_background.dart'; 11 | import 'package:do_not_disturb/do_not_disturb.dart'; 12 | import 'package:flutter_settings_screens/flutter_settings_screens.dart'; 13 | import 'package:get_it/get_it.dart'; 14 | import 'package:meditation/main.dart'; 15 | import 'package:shared_preferences/shared_preferences.dart'; 16 | import 'package:wakelock_plus/wakelock_plus.dart'; 17 | 18 | import 'package:meditation/audioplayer.dart'; 19 | import 'package:meditation/utils.dart'; 20 | 21 | enum TimerState { stopped, delaying, meditating } 22 | 23 | const int endingNotificationID = 100; 24 | const int startingNotificationID = 101; 25 | // intervalNotification is also going to use up the next few numbers 26 | // reserve 200-299 for it 27 | const int intervalNotificationID = 200; 28 | 29 | class NotificationController { 30 | /// Use this method to detect when a new notification or a schedule is created 31 | @pragma("vm:entry-point") 32 | static Future onNotificationCreatedMethod( 33 | ReceivedNotification receivedNotification) async {} 34 | 35 | /// Use this method to detect every time that a new notification is displayed 36 | @pragma("vm:entry-point") 37 | static Future onNotificationDisplayedMethod( 38 | ReceivedNotification receivedNotification) async { 39 | log.i("notification displayed. id = ${receivedNotification.id}"); 40 | eventBus.fire(NotificationEvent("displayed", receivedNotification.id)); 41 | } 42 | 43 | /// Use this method to detect if the user dismissed a notification 44 | @pragma("vm:entry-point") 45 | static Future onDismissActionReceivedMethod(ReceivedAction receivedAction) async {} 46 | 47 | /// Use this method to detect when the user taps on a notification or action button 48 | @pragma("vm:entry-point") 49 | static Future onActionReceivedMethod(ReceivedAction receivedAction) async {} 50 | } 51 | 52 | class TimerWidget extends StatefulWidget { 53 | const TimerWidget({Key? key}) : super(key: key); 54 | 55 | @override 56 | State createState() => _TimerWidgetState(); 57 | } 58 | 59 | class _TimerWidgetState extends State with SingleTickerProviderStateMixin { 60 | final List intervalNotificationIDs = []; 61 | 62 | final dndPlugin = DoNotDisturbPlugin(); 63 | 64 | late Ticker ticker; 65 | 66 | TimerState timerState = TimerState.stopped; 67 | int timerMinutes = 0; 68 | int timerDelaySeconds = 0; 69 | String timerButtonText = "begin"; 70 | 71 | DateTime startTime = DateTime.now(); 72 | DateTime endTime = DateTime.now(); 73 | Duration timeLeft = const Duration(minutes: 0, seconds: 0); 74 | double timerProgress = 0.0; 75 | 76 | bool intervalsEnabled = false; 77 | Duration intervalTime = const Duration(minutes: 0); 78 | int intervalCount = 0; 79 | 80 | @override 81 | void initState() { 82 | super.initState(); 83 | 84 | ticker = createTicker(tickerUpdate); 85 | // timerDelayTicker = createTicker(timerDelayUpdate); 86 | 87 | SharedPreferences.getInstance().then((prefs) { 88 | setState(() { 89 | timerMinutes = prefs.getInt('timer-minutes') ?? 0; 90 | }); 91 | }); 92 | 93 | AwesomeNotifications().setListeners( 94 | onActionReceivedMethod: NotificationController.onActionReceivedMethod, 95 | onNotificationCreatedMethod: NotificationController.onNotificationCreatedMethod, 96 | onNotificationDisplayedMethod: NotificationController.onNotificationDisplayedMethod, 97 | onDismissActionReceivedMethod: NotificationController.onDismissActionReceivedMethod); 98 | 99 | eventBus.on().listen((notificationEvent) { 100 | // All events are of type UserLoggedInEvent (or subtypes of it). 101 | log.d( 102 | "received notification event - type: ${notificationEvent.type}, id: ${notificationEvent.id}"); 103 | actOnNotificationDisplayed(notificationEvent.id); 104 | }); 105 | } 106 | 107 | @override 108 | void dispose() { 109 | AwesomeNotifications().cancelAll(); 110 | ticker.dispose(); 111 | 112 | super.dispose(); 113 | } 114 | 115 | void actOnNotificationDisplayed(int? notificationID) { 116 | // on ios this doesn't seem to get called immediately 117 | // so if the app isn't in the foreground, onTimerEnd execution is delayed 118 | // this likely doesn't allow the sound to play on time 119 | // so you'd rather have to play it through the notification 120 | // or find another way of executing code when the app is not in the foreground 121 | 122 | if (notificationID == endingNotificationID) { 123 | if (timerState == TimerState.meditating) { 124 | log.i( 125 | "ending timer through displayedStream notification callback. timeleft: ${timeLeft.inMilliseconds} ms"); 126 | onTimerEnd(); 127 | } 128 | } 129 | if (intervalNotificationIDs.contains(notificationID)) { 130 | if (timerState == TimerState.meditating) { 131 | log.i("reached interval timer through displayedStream notification callback"); 132 | onTimerInterval(); 133 | Timer(const Duration(seconds: 0), () { 134 | log.i("dismissing interval notification"); 135 | AwesomeNotifications().dismiss(notificationID!); 136 | }); 137 | } 138 | } 139 | if (notificationID == startingNotificationID) { 140 | if (timerState == TimerState.delaying) { 141 | log.i("reached starting timer through displayedStream notification callback"); 142 | onMeditationStart(); 143 | Timer(const Duration(seconds: 4), () { 144 | log.i("dismissing start notification"); 145 | AwesomeNotifications().dismiss(startingNotificationID); 146 | }); 147 | } 148 | } 149 | } 150 | 151 | // this function only gets called if the screen is on 152 | void tickerUpdate(Duration elapsed) { 153 | if (timerState == TimerState.stopped) { 154 | return; 155 | } 156 | 157 | if (timerState == TimerState.delaying) { 158 | // delaying 159 | 160 | var countdown = timerDelaySeconds - elapsed.inSeconds; 161 | 162 | setState(() { 163 | timerButtonText = countdown.toString(); 164 | }); 165 | 166 | // if (elapsed.inSeconds > timerDelaySeconds - 1) { 167 | // // done delaying 168 | // onMeditationStart(); 169 | // } 170 | } 171 | 172 | if (timerState == TimerState.meditating) { 173 | // meditating 174 | 175 | // backup system for ending the timer in case notification fails 176 | if (timeLeft.inMilliseconds <= 200) { 177 | // better for this to end a little early than too late 178 | // not sure how fast ticker is called but maybe 60fps? so 16.6 ms between calls 179 | // 200ms allows for ~12 frames leeway 180 | log.i("ending timer through timerUpdate"); 181 | onTimerEnd(); 182 | return; 183 | } 184 | 185 | var timeElapsed = DateTime.now().difference(startTime); 186 | 187 | // if (intervalsEnabled && intervalCount > 0) { 188 | // if (timeLeft <= intervalTime * intervalCount) { 189 | // intervalCount -= 1; 190 | // onTimerInterval(); 191 | // } 192 | // } 193 | 194 | // invlerp: t = (v-a) / (b-a); 195 | int vma = timeElapsed.inMilliseconds; 196 | int bma = endTime.difference(startTime).inMilliseconds; 197 | double t = vma / bma; 198 | 199 | setState(() { 200 | timerProgress = t; 201 | timeLeft = timeLeftTo(endTime); 202 | }); 203 | } 204 | } 205 | 206 | // returns true if permissions ok 207 | Future checkIfPermissionsOkay() async { 208 | var userAction = false; 209 | 210 | if (Settings.getValue('dnd') == true) { 211 | bool hasAccess = await dndPlugin.isNotificationPolicyAccessGranted(); 212 | if (!hasAccess) { 213 | await requestPermissionDND(context, dndPlugin, suggestDisable: true); 214 | userAction = true; 215 | 216 | // return early to let user disable dnd if needed 217 | return !userAction; 218 | } 219 | } 220 | 221 | var backgroundPermissionOkay = await FlutterBackground.hasPermissions; 222 | if (!backgroundPermissionOkay) { 223 | await requestBatteryOptimization(context); 224 | userAction = true; 225 | 226 | backgroundPermissionOkay = await FlutterBackground.hasPermissions; 227 | if (!backgroundPermissionOkay) { 228 | // if the user hasn't enabled the permission, return early 229 | // because without batteryoptimization permissions we can't enable precisealarms notification 230 | return !userAction; 231 | } 232 | } 233 | 234 | // request app permissions. this should be enough 235 | var (requestedUserAction, allowedPermissions) = 236 | await requestNotificationPermissions(context, channelKey: null, permissionList: [ 237 | // generic default permissions are alert, sound, vibration, light 238 | NotificationPermission.Alert, 239 | // we remove badge support 240 | // NotificationPermission.Badge, 241 | NotificationPermission.Sound, 242 | NotificationPermission.Vibration, 243 | NotificationPermission.Light, 244 | // extra permissions 245 | NotificationPermission.CriticalAlert, 246 | NotificationPermission.FullScreenIntent, 247 | // precise alarms requires disabling of battery optimization? 248 | NotificationPermission.PreciseAlarms, 249 | // NotificationPermission.OverrideDnD, 250 | ]); 251 | if (requestedUserAction) { 252 | userAction = true; 253 | } 254 | 255 | var channelHelper = ChannelHelper(); 256 | 257 | bool isChannelEnabled = await channelHelper.isNotificationChannelEnabled("timer-main"); 258 | log.d("is channel timer-main enabled: $isChannelEnabled"); 259 | if (!isChannelEnabled) { 260 | userAction = true; 261 | await requestUserToEnableChannel(context, "timer-main"); 262 | } 263 | 264 | isChannelEnabled = await channelHelper.isNotificationChannelEnabled("timer-support"); 265 | log.d("is channel timer-support enabled: $isChannelEnabled"); 266 | if (!isChannelEnabled) { 267 | userAction = true; 268 | await requestUserToEnableChannel(context, "timer-support"); 269 | } 270 | 271 | // these checks are extra checks to see if for any reason any of the individual channels was disabled 272 | // these extra checks are to see if we can enable the requested permissions 273 | // note that in case of user tampering with permissions, these can't be restored at all 274 | // but these permissions seem to be aesthetic so it should not affect us much 275 | // we use the Light permission because we have to check at least one 276 | // and this is the least intrusive that we can enable 277 | // but if only checking Light to check for channel activation, then the above checks are enough 278 | (requestedUserAction, allowedPermissions) = await requestNotificationPermissions(context, 279 | channelKey: "timer-main", 280 | permissionList: [NotificationPermission.Alert, NotificationPermission.Light]); 281 | if (requestedUserAction) { 282 | userAction = true; 283 | } 284 | 285 | // testing for Light is a way of simply testing whether the notification is enabled or not 286 | // vibration and sound won't work on testing for a min importance notification channel 287 | (requestedUserAction, allowedPermissions) = await requestNotificationPermissions(context, 288 | channelKey: "timer-support", permissionList: [NotificationPermission.Light]); 289 | if (requestedUserAction) { 290 | userAction = true; 291 | } 292 | 293 | // if we asked user for stuff, consider the check to have failed, letting user press start again 294 | return !userAction; 295 | } 296 | 297 | void onTimerButtonPress() async { 298 | if (timerState == TimerState.stopped) { 299 | // start pressed 300 | var permissionsOkay = await checkIfPermissionsOkay(); 301 | 302 | if (permissionsOkay) { 303 | // all permissions are good. start! 304 | onTimerStart(); 305 | } else { 306 | // user was prompted. let them press start button again 307 | return; 308 | } 309 | } else { 310 | // stop pressed 311 | if (timerDelaySeconds == 0 && DateTime.now().difference(startTime).inMilliseconds < 500) { 312 | // prevent accidental double tap 313 | // but not if coming from the delayed start 314 | return; 315 | } 316 | // AwesomeNotifications().cancelSchedule(endingNotificationID); 317 | AwesomeNotifications().cancel(endingNotificationID); 318 | AwesomeNotifications().cancel(intervalNotificationID); 319 | // onTimerEnd(playAudio: true); 320 | if (kReleaseMode) { 321 | onTimerEnd(playAudio: false); 322 | } else { 323 | onTimerEnd(playAudio: true); 324 | } 325 | } 326 | } 327 | 328 | void onTimerInterval() async { 329 | if (timerState != TimerState.meditating) { 330 | log.e("onTimerInterval called after meditation finished"); 331 | return; 332 | } 333 | 334 | log.d("playing interval sound at $timeLeft"); 335 | 336 | NAudioPlayer audioPlayer = GetIt.I.get(); 337 | audioPlayer.playSound('interval-sound'); 338 | } 339 | 340 | // this happens on either start or delay 341 | void onTimerStart() async { 342 | // cleaning up endingnotification in case it's still around 343 | // 10s timer for dismissal at onTimerEnd will still call and presumably do nothing 344 | // probably no need to fix that 345 | AwesomeNotifications().dismiss(endingNotificationID); 346 | 347 | await initFlutterBackground(); 348 | if (Platform.isAndroid) { 349 | bool backgroundSuccess = await FlutterBackground.enableBackgroundExecution(); 350 | log.i('enable background success: $backgroundSuccess'); 351 | } 352 | 353 | if (Settings.getValue('dnd') == true) { 354 | log.i('enabling dnd'); 355 | // we already checked permissions 356 | await dndPlugin.setInterruptionFilter(InterruptionFilter.alarms); 357 | } 358 | 359 | if (Settings.getValue('delay-enabled') ?? false) { 360 | timerDelaySeconds = int.parse(Settings.getValue('delay-time') ?? '5'); 361 | // unconditionally force screen wakelock for the delay part 362 | // this won't prevent manual turn off of display, but will save us from a short screen-off time 363 | // for the meditation part, wakelock can be user-set 364 | WakelockPlus.enable(); 365 | // cannot send notifications at intervals <5 366 | // what happens with less than 5 seconds is unclear, so we prevent that 367 | await scheduleStartingNotification(timerDelaySeconds); 368 | // start delay 369 | timerState = TimerState.delaying; 370 | // onMeditationStart() will be called at the end of the elapsed time 371 | } else { 372 | // start meditation 373 | onMeditationStart(); 374 | } 375 | 376 | ticker.start(); 377 | } 378 | 379 | // pretty much just the visual/aural part and the switch of state 380 | void onMeditationStart() async { 381 | var timerDuration = Duration(minutes: timerMinutes); 382 | if (timerMinutes == 0) { 383 | // this is the test mode 384 | timerDuration += Duration(seconds: 10); 385 | } 386 | 387 | startTime = DateTime.now(); 388 | endTime = startTime.add(timerDuration); 389 | 390 | // initial calculation. must happen before we start the ticker 391 | // we need to calculate timeleft before switching to meditating state 392 | // otherwise the first tickerupdate could run with the wrong timeleft 393 | timeLeft = timeLeftTo(endTime); 394 | 395 | bool wakelockEnabled = await WakelockPlus.enabled; 396 | if (Settings.getValue('screen-wakelock') == true) { 397 | if (wakelockEnabled) { 398 | // wakelock was already setup in onTimerStart 399 | log.i('maintaining screen wakelock for meditation'); 400 | } else { 401 | log.e('wakelock is not enabled but was supposed to be'); 402 | } 403 | } else { 404 | if (wakelockEnabled) { 405 | log.i('disabling screen wakelock for meditation'); 406 | WakelockPlus.disable(); 407 | } 408 | } 409 | 410 | // start the meditation 411 | 412 | timerState = TimerState.meditating; 413 | setState(() { 414 | timerButtonText = "end"; 415 | }); 416 | 417 | await scheduleEndingNotification(timerDuration.inSeconds); 418 | 419 | intervalsEnabled = Settings.getValue('intervals-enabled') ?? false; 420 | if (intervalsEnabled) { 421 | intervalTime = 422 | Duration(minutes: int.parse(Settings.getValue('interval-time') ?? '0')); 423 | if (intervalTime.inMinutes >= 1) { 424 | log.i('enabling intervals'); 425 | var diff = endTime.difference(startTime); 426 | intervalCount = (diff.inMinutes / intervalTime.inMinutes).floor(); 427 | if (diff.inMinutes % intervalTime.inMinutes == 0) { 428 | // if no remainder, then we remove the last count 429 | // which would happen together with the end bell 430 | intervalCount -= 1; 431 | } 432 | log.d("interval count: $intervalCount"); 433 | intervalNotificationIDs.clear(); 434 | for (var i = 1; i <= intervalCount; i++) { 435 | var id = intervalNotificationID + i; 436 | log.d( 437 | "scheduling interval notification with id $id " + "in ${i * intervalTime.inMinutes}"); 438 | await scheduleIntervalNotification(i * intervalTime.inSeconds, id: id); 439 | intervalNotificationIDs.add(id); 440 | } 441 | } 442 | } 443 | 444 | NAudioPlayer audioPlayer = GetIt.I.get(); 445 | audioPlayer.playSound('start-sound'); 446 | 447 | ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Meditation started'))); 448 | } 449 | 450 | void onTimerEnd({bool playAudio = true}) async { 451 | // log.i("timer-end", "called"); 452 | // making sure this doesn't get called twice 453 | // since we do use the backup check on timerUpdate 454 | // as well as the notification 455 | if (timerState == TimerState.stopped) { 456 | // if (!meditating) { 457 | log.e("onTimerEnd called when it shouldn't have"); 458 | return; 459 | } 460 | 461 | setState(() { 462 | // one last update so we know how much we were off on timeout 463 | if (timerState == TimerState.meditating) { 464 | log.d("ending meditation at timeLeft: $timeLeft"); 465 | timeLeft = timeLeftTo(endTime, roundToZero: true); 466 | } 467 | timerButtonText = "begin"; 468 | }); 469 | 470 | timerState = TimerState.stopped; 471 | ticker.stop(); 472 | 473 | // removing 'meditation started' snackbar in case it's still showing 474 | ScaffoldMessenger.of(context).removeCurrentSnackBar(); 475 | 476 | WakelockPlus.disable(); 477 | 478 | if (Settings.getValue('dnd') == true) { 479 | log.i('disabling dnd'); 480 | await dndPlugin.setInterruptionFilter(InterruptionFilter.all); 481 | // await FlutterDnd.setInterruptionFilter( 482 | // FlutterDnd.INTERRUPTION_FILTER_ALL); 483 | } 484 | 485 | if (Platform.isAndroid) { 486 | if (FlutterBackground.isBackgroundExecutionEnabled) { 487 | bool backgroundSuccess = await FlutterBackground.disableBackgroundExecution(); 488 | log.i("disable flutter_background: success = $backgroundSuccess"); 489 | } else { 490 | log.e("background wasn't enabled. why?"); 491 | } 492 | } 493 | 494 | if (Platform.isAndroid) { 495 | // only dismiss on android 496 | // why do we want to dismiss? 497 | // Timer(const Duration(seconds: 10), () { 498 | // log.i("dismissing end notification"); 499 | // AwesomeNotifications().dismiss(endingNotificationID); 500 | // }); 501 | } 502 | 503 | AwesomeNotifications().cancelSchedulesByGroupKey('timer-interval'); 504 | 505 | NAudioPlayer audioPlayer = GetIt.I.get(); 506 | if (playAudio) { 507 | audioPlayer.playSound('end-sound'); 508 | } else { 509 | audioPlayer.stopPrevious(); 510 | } 511 | } 512 | 513 | Future scheduleEndingNotification(int intervalSeconds) async { 514 | String localTimeZone = await AwesomeNotifications().getLocalTimeZoneIdentifier(); 515 | AwesomeNotifications().createNotification( 516 | content: NotificationContent( 517 | id: endingNotificationID, 518 | channelKey: 'timer-main', 519 | groupKey: 'timer-end', 520 | title: 'Meditation ended', 521 | body: 'Swipe to dismiss', 522 | // icon: 'resource://drawable/ic_notification', 523 | largeIcon: 'resource://mipmap/ic_launcher', 524 | // Alarm and Event seem to both show up in dnd mode (this is what we want) 525 | // Alarm also makes the notification undismissable with swiping and requires interaction 526 | // which we probably don't want 527 | // Event instead is dismissable 528 | // category: NotificationCategory.Alarm, 529 | category: NotificationCategory.Event, 530 | // notificationLayout: NotificationLayout.Default, 531 | notificationLayout: NotificationLayout.BigPicture, 532 | // criticalAlert: play sounds even when in dnd. likely only useful for ios 533 | criticalAlert: true, 534 | // autoDismissible: gets dismissed on tap (meaning even when it opens the app, it dismisses itself) 535 | // if false, tapping will open the app but the notification stays 536 | autoDismissible: true, 537 | // wakeUpScreen: wake up screen even when locked 538 | // shows app in fullscreen even from locked (but only if phone was locked with app in foreground) 539 | wakeUpScreen: true, 540 | // fullScreenIntent keeps showing the notification popup in front permanently until user dismisses it 541 | // for some reason fullscreenintent makes the notification not appear 542 | fullScreenIntent: false, 543 | // dismiss this automatically after a few seconds. or maybe better not? i quite like seeing the confirmation of end 544 | // timeoutAfter: Duration(seconds: 10), 545 | // don't open the app on tap 546 | actionType: ActionType.DisabledAction, 547 | ), 548 | schedule: NotificationInterval( 549 | interval: Duration(seconds: intervalSeconds), 550 | timeZone: localTimeZone, 551 | allowWhileIdle: true, 552 | preciseAlarm: true), 553 | // actionButtons: [ 554 | // NotificationActionButton( 555 | // key: 'dismiss', 556 | // label: 'Dismiss', 557 | // autoDismissible: true, 558 | // ), 559 | // ], 560 | ); 561 | } 562 | 563 | Future scheduleStartingNotification(int intervalSeconds) async { 564 | String localTimeZone = await AwesomeNotifications().getLocalTimeZoneIdentifier(); 565 | AwesomeNotifications().createNotification( 566 | content: NotificationContent( 567 | id: startingNotificationID, 568 | channelKey: 'timer-support', 569 | groupKey: 'timer-start', 570 | title: 'Meditation started', 571 | body: 'Tap to return to app', 572 | // icon: 'resource://drawable/ic_notification', 573 | largeIcon: 'resource://mipmap/ic_launcher', 574 | category: NotificationCategory.Event, 575 | notificationLayout: NotificationLayout.BigPicture, 576 | criticalAlert: false, 577 | autoDismissible: true, 578 | wakeUpScreen: false, 579 | fullScreenIntent: false, 580 | ), 581 | schedule: NotificationInterval( 582 | interval: Duration(seconds: intervalSeconds), 583 | timeZone: localTimeZone, 584 | allowWhileIdle: true, 585 | preciseAlarm: true), 586 | ); 587 | } 588 | 589 | Future scheduleIntervalNotification(int intervalSeconds, 590 | {int id = intervalNotificationID}) async { 591 | String localTimeZone = await AwesomeNotifications().getLocalTimeZoneIdentifier(); 592 | AwesomeNotifications().createNotification( 593 | content: NotificationContent( 594 | id: id, 595 | channelKey: 'timer-support', 596 | groupKey: 'timer-interval', 597 | title: 'Meditation interval', 598 | body: 'Tap to return to app', 599 | icon: 'resource://drawable/ic_notification', 600 | largeIcon: 'resource://mipmap/ic_launcher', 601 | category: NotificationCategory.Event, 602 | notificationLayout: NotificationLayout.BigPicture, 603 | criticalAlert: true, 604 | autoDismissible: true, 605 | wakeUpScreen: true, 606 | fullScreenIntent: false, 607 | ), 608 | schedule: NotificationInterval( 609 | interval: Duration(seconds: intervalSeconds), 610 | timeZone: localTimeZone, 611 | allowWhileIdle: true, 612 | preciseAlarm: true), 613 | ); 614 | } 615 | 616 | Future showTimeChoice() async { 617 | var minutes; 618 | var timeChoices = [5, 10, 15, 20, 25, 30, 45, 60]; 619 | 620 | var _selected = await showDialog( 621 | context: context, 622 | builder: (BuildContext context) { 623 | return SimpleDialog( 624 | title: const Text('Select time'), 625 | //@formatter:off 626 | children: [ 627 | if (!kReleaseMode) SimpleDialogOption( 628 | onPressed: () { Navigator.pop(context, 0); }, 629 | child: const Text('0 - 10s'), 630 | ), 631 | SimpleDialogOption( 632 | onPressed: () { Navigator.pop(context, 1); }, 633 | child: const Text('1 minute'), 634 | ), 635 | for (var t in timeChoices) SimpleDialogOption( 636 | onPressed: () { Navigator.pop(context, t); }, 637 | child: Text('$t minutes'), 638 | ), 639 | SimpleDialogOption( 640 | onPressed: () { Navigator.pop(context, -1); }, 641 | child: const Text('custom...'), 642 | ), 643 | ], 644 | //@formatter:on 645 | ); 646 | }); 647 | 648 | if (_selected == null) { 649 | // we just closed the dialog without choosing anything 650 | return; 651 | } 652 | 653 | if (_selected >= 0) { 654 | // preset chosen 655 | minutes = _selected; 656 | } else if (_selected == -1) { 657 | // inputting custom time 658 | final Color borderColor = primaryColor!; 659 | final Color errorColor = secondaryColor!; 660 | final GlobalKey formKey = GlobalKey(); 661 | final controller = TextEditingController(); 662 | var cancelled = true; 663 | 664 | await showDialog( 665 | context: context, 666 | builder: (BuildContext context) { 667 | return SimpleDialog( 668 | title: const Text('Input time'), 669 | children: [ 670 | Container( 671 | padding: EdgeInsets.all(16), 672 | // do we need the form? 673 | child: Form( 674 | key: formKey, 675 | child: TextFormField( 676 | keyboardType: TextInputType.number, 677 | controller: controller, 678 | autofocus: true, 679 | autovalidateMode: AutovalidateMode.onUserInteraction, 680 | inputFormatters: [ 681 | FilteringTextInputFormatter.digitsOnly, 682 | FilteringTextInputFormatter.deny(RegExp(r"^0")), 683 | ], 684 | validator: timeInputValidatorConstructor( 685 | minTimerTime: 1, maxTimerTime: maxMeditationTime) as Validator, 686 | decoration: InputDecoration( 687 | helperText: "Input time in minutes, between 1 and $maxMeditationTime.", 688 | errorMaxLines: 3, 689 | helperMaxLines: 3, 690 | errorStyle: TextStyle( 691 | color: errorColor, 692 | ), 693 | errorBorder: OutlineInputBorder( 694 | borderRadius: BorderRadius.all( 695 | Radius.circular(5.0), 696 | ), 697 | borderSide: BorderSide(color: errorColor), 698 | ), 699 | border: OutlineInputBorder( 700 | borderRadius: BorderRadius.all( 701 | Radius.circular(5.0), 702 | ), 703 | borderSide: BorderSide( 704 | color: borderColor, 705 | ), 706 | ), 707 | focusedBorder: OutlineInputBorder( 708 | borderRadius: BorderRadius.all( 709 | Radius.circular(5.0), 710 | ), 711 | borderSide: BorderSide( 712 | color: borderColor, 713 | ), 714 | ), 715 | ), 716 | ), 717 | ), 718 | ), 719 | ButtonBar( 720 | alignment: MainAxisAlignment.end, 721 | children: [ 722 | TextButton( 723 | child: Text("CANCEL"), 724 | onPressed: () { 725 | Navigator.pop(context); 726 | }, 727 | ), 728 | TextButton( 729 | child: Text("OK"), 730 | onPressed: () { 731 | if (formKey.currentState!.validate()) { 732 | cancelled = false; 733 | Navigator.pop(context); 734 | } 735 | }, 736 | ), 737 | ], 738 | ), 739 | ], 740 | ); 741 | }); 742 | 743 | if (cancelled) { 744 | // pressed cancel or outside the dialog 745 | return; 746 | } else { 747 | minutes = int.parse(controller.text); 748 | } 749 | } 750 | 751 | if (minutes == null) { 752 | log.e("we didn't get to choose any value for minutes. something went very wrong"); 753 | } 754 | 755 | setState(() { 756 | timerMinutes = minutes; 757 | }); 758 | final prefs = await SharedPreferences.getInstance(); 759 | prefs.setInt('timer-minutes', timerMinutes); 760 | } 761 | 762 | @override 763 | Widget build(BuildContext context) { 764 | return Stack( 765 | alignment: AlignmentDirectional.center, 766 | children: [ 767 | if (Settings.getValue('show-countdown') == true) 768 | Align( 769 | alignment: Alignment(0, -0.70), 770 | child: Row( 771 | mainAxisAlignment: MainAxisAlignment.center, 772 | children: [ 773 | Text(timeLeftString(timeLeft), style: Theme.of(context).textTheme.bodyLarge), 774 | ], 775 | )), 776 | SizedBox( 777 | width: MediaQuery.of(context).size.shortestSide / 1.5, 778 | height: MediaQuery.of(context).size.shortestSide / 1.5, 779 | child: Stack( 780 | children: [ 781 | Align( 782 | alignment: Alignment(0, -0.1), 783 | child: Stack( 784 | alignment: AlignmentDirectional.center, 785 | children: [ 786 | Container( 787 | width: MediaQuery.of(context).size.shortestSide / 1.5, 788 | height: MediaQuery.of(context).size.shortestSide / 1.5, 789 | child: CircularProgressIndicator( 790 | value: timerState == TimerState.meditating ? timerProgress : 0.0, 791 | strokeWidth: 10, 792 | ), 793 | ), 794 | TextButton( 795 | style: timerButtonStyle, 796 | onPressed: () { 797 | onTimerButtonPress(); 798 | }, 799 | child: Container( 800 | width: MediaQuery.of(context).size.shortestSide / 1.75, 801 | height: MediaQuery.of(context).size.shortestSide / 1.75, 802 | alignment: Alignment.center, 803 | decoration: BoxDecoration(shape: BoxShape.circle), 804 | child: Text(timerButtonText.toUpperCase()), 805 | ), 806 | ), 807 | ], 808 | ), 809 | ), 810 | ], 811 | ), 812 | ), 813 | Align( 814 | alignment: Alignment(0, 0.75), 815 | child: TextButton( 816 | style: timeSelectionButtonStyle, 817 | onPressed: () { 818 | showTimeChoice(); 819 | }, 820 | child: Row( 821 | mainAxisAlignment: MainAxisAlignment.center, 822 | mainAxisSize: MainAxisSize.min, 823 | children: [ 824 | Icon( 825 | Icons.schedule, 826 | size: 30, 827 | ), 828 | Text( 829 | ' ${timerMinutes}m', 830 | ) 831 | ], 832 | ), 833 | ), 834 | ), 835 | ], 836 | ); 837 | } 838 | } 839 | --------------------------------------------------------------------------------