├── 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 | [
](https://f-droid.org/packages/com.nyxkn.meditation/)
11 |
12 | [
](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 | 
23 | 
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 | [](https://www.buymeacoffee.com/nyxkn)
62 |
63 | [](https://liberapay.com/nyxkn)
64 |
65 | [](https://ko-fi.com/nyxkn)
66 |
67 | [](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 |
--------------------------------------------------------------------------------