├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── resources.properties
│ │ │ ├── font
│ │ │ │ ├── one_ui_sans_400.ttf
│ │ │ │ ├── one_ui_sans_600.ttf
│ │ │ │ └── one_ui_sans_700.ttf
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── dp_hridayan.png
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── ic_launcher_background.png
│ │ │ │ ├── ic_launcher_foreground.png
│ │ │ │ └── ic_launcher_monochrome.png
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── ic_launcher_background.png
│ │ │ │ ├── ic_launcher_foreground.png
│ │ │ │ └── ic_launcher_monochrome.png
│ │ │ ├── raw
│ │ │ │ └── spinning_gears.lottie
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── ic_launcher_background.png
│ │ │ │ ├── ic_launcher_foreground.png
│ │ │ │ └── ic_launcher_monochrome.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── ic_launcher_background.png
│ │ │ │ ├── ic_launcher_foreground.png
│ │ │ │ └── ic_launcher_monochrome.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── ic_launcher_background.png
│ │ │ │ ├── ic_launcher_foreground.png
│ │ │ │ └── ic_launcher_monochrome.png
│ │ │ ├── xml
│ │ │ │ ├── file_paths.xml
│ │ │ │ ├── backup_rules.xml
│ │ │ │ └── data_extraction_rules.xml
│ │ │ ├── values
│ │ │ │ └── themes.xml
│ │ │ ├── mipmap-anydpi
│ │ │ │ └── ic_launcher.xml
│ │ │ └── drawable
│ │ │ │ ├── ic_version.xml
│ │ │ │ ├── ic_mail.xml
│ │ │ │ ├── ic_format_italic.xml
│ │ │ │ ├── ic_license.xml
│ │ │ │ ├── ic_telegram.xml
│ │ │ │ ├── ic_format_clear.xml
│ │ │ │ ├── ic_format_bold.xml
│ │ │ │ ├── ic_format_underline.xml
│ │ │ │ ├── ic_check_circle.xml
│ │ │ │ ├── ic_font_download.xml
│ │ │ │ ├── ic_info.xml
│ │ │ │ ├── ic_add_comment.xml
│ │ │ │ ├── ic_github.xml
│ │ │ │ ├── ic_matchcase.xml
│ │ │ │ ├── ic_handyman.xml
│ │ │ │ ├── ic_report.xml
│ │ │ │ ├── ic_database.xml
│ │ │ │ ├── ic_notifications_important.xml
│ │ │ │ ├── ic_upload_file.xml
│ │ │ │ ├── ic_notifications.xml
│ │ │ │ ├── ic_restore_page.xml
│ │ │ │ ├── ic_release_alert.xml
│ │ │ │ ├── ic_upcoming.xml
│ │ │ │ ├── ic_reset_settings.xml
│ │ │ │ └── ic_settings.xml
│ │ ├── java
│ │ │ └── in
│ │ │ │ └── hridayan
│ │ │ │ └── driftly
│ │ │ │ ├── core
│ │ │ │ ├── domain
│ │ │ │ │ ├── model
│ │ │ │ │ │ ├── StreakType.kt
│ │ │ │ │ │ ├── GithubReleaseType.kt
│ │ │ │ │ │ ├── SubjectCardStyle.kt
│ │ │ │ │ │ ├── TotalAttendance.kt
│ │ │ │ │ │ ├── AttendanceStatus.kt
│ │ │ │ │ │ ├── SubjectAttendance.kt
│ │ │ │ │ │ ├── SubjectError.kt
│ │ │ │ │ │ ├── NotificationTags.kt
│ │ │ │ │ │ └── DownloadState.kt
│ │ │ │ │ ├── repository
│ │ │ │ │ │ ├── DownloadRepository.kt
│ │ │ │ │ │ ├── SubjectRepository.kt
│ │ │ │ │ │ └── AttendanceRepository.kt
│ │ │ │ │ ├── provider
│ │ │ │ │ │ └── SeedColorProvider.kt
│ │ │ │ │ └── usecase
│ │ │ │ │ │ └── DownloadApkUseCase.kt
│ │ │ │ ├── presentation
│ │ │ │ │ ├── components
│ │ │ │ │ │ ├── svg
│ │ │ │ │ │ │ └── _DrawableVectors.kt
│ │ │ │ │ │ ├── progress
│ │ │ │ │ │ │ ├── LoadingSpinner.kt
│ │ │ │ │ │ │ ├── CircularProgressWithText.kt
│ │ │ │ │ │ │ └── AnimatedCircularProgressIndicator.kt
│ │ │ │ │ │ ├── tooltip
│ │ │ │ │ │ │ └── TooltipContent.kt
│ │ │ │ │ │ ├── button
│ │ │ │ │ │ │ └── BackButton.kt
│ │ │ │ │ │ ├── checkbox
│ │ │ │ │ │ │ └── CheckboxWithText.kt
│ │ │ │ │ │ ├── card
│ │ │ │ │ │ │ └── RoundedCornerCard.kt
│ │ │ │ │ │ ├── lottie
│ │ │ │ │ │ │ └── SpinningGearsLottie.kt
│ │ │ │ │ │ ├── canvas
│ │ │ │ │ │ │ ├── HorizontalProgressWave.kt
│ │ │ │ │ │ │ └── VerticalProgressWave.kt
│ │ │ │ │ │ └── text
│ │ │ │ │ │ │ └── AutoResizeableText.kt
│ │ │ │ │ ├── theme
│ │ │ │ │ │ └── Shape.kt
│ │ │ │ │ └── provider
│ │ │ │ │ │ └── FontProvider.kt
│ │ │ │ ├── di
│ │ │ │ │ ├── qualifiers
│ │ │ │ │ │ └── ApiHttpClient.kt
│ │ │ │ │ ├── entry
│ │ │ │ │ │ └── WorkerEntryPoint.kt
│ │ │ │ │ ├── UseCaseModule.kt
│ │ │ │ │ ├── DatabaseModule.kt
│ │ │ │ │ ├── RepositoryModule.kt
│ │ │ │ │ ├── SettingsModule.kt
│ │ │ │ │ └── NetworkModule.kt
│ │ │ │ ├── data
│ │ │ │ │ ├── model
│ │ │ │ │ │ ├── SubjectEntity.kt
│ │ │ │ │ │ └── AttendanceEntity.kt
│ │ │ │ │ ├── database
│ │ │ │ │ │ ├── Migrations.kt
│ │ │ │ │ │ ├── Converters.kt
│ │ │ │ │ │ ├── SubjectDatabase.kt
│ │ │ │ │ │ └── SubjectDao.kt
│ │ │ │ │ └── repository
│ │ │ │ │ │ ├── SubjectRepositoryImpl.kt
│ │ │ │ │ │ └── AttendanceRepositoryImpl.kt
│ │ │ │ ├── utils
│ │ │ │ │ ├── HapticUtils.kt
│ │ │ │ │ ├── ApkInstaller.kt
│ │ │ │ │ ├── FileUtils.kt
│ │ │ │ │ ├── PaletteExtensions.kt
│ │ │ │ │ ├── Utils.kt
│ │ │ │ │ ├── EncryptionHelper.kt
│ │ │ │ │ └── MiUiCheck.kt
│ │ │ │ └── common
│ │ │ │ │ ├── constants
│ │ │ │ │ └── UrlConst.kt
│ │ │ │ │ └── LocalSettings.kt
│ │ │ │ ├── settings
│ │ │ │ ├── domain
│ │ │ │ │ ├── model
│ │ │ │ │ │ ├── SettingsType.kt
│ │ │ │ │ │ ├── ChangelogItem.kt
│ │ │ │ │ │ ├── RadioButtonOptions.kt
│ │ │ │ │ │ ├── GithubRelease.kt
│ │ │ │ │ │ ├── BackupOption.kt
│ │ │ │ │ │ ├── CustomFontFamily.kt
│ │ │ │ │ │ ├── NotificationState.kt
│ │ │ │ │ │ ├── UpdateResult.kt
│ │ │ │ │ │ ├── BackupData.kt
│ │ │ │ │ │ ├── PreferenceGroup.kt
│ │ │ │ │ │ └── SettingsState.kt
│ │ │ │ │ ├── repository
│ │ │ │ │ │ ├── UpdateRepository.kt
│ │ │ │ │ │ ├── BackupAndRestoreRepository.kt
│ │ │ │ │ │ └── SettingsRepository.kt
│ │ │ │ │ └── usecase
│ │ │ │ │ │ ├── ToggleSettingUseCase.kt
│ │ │ │ │ │ ├── GetAllChangelogsUseCase.kt
│ │ │ │ │ │ └── CheckUpdateUseCase.kt
│ │ │ │ ├── data
│ │ │ │ │ ├── local
│ │ │ │ │ │ ├── provider
│ │ │ │ │ │ │ └── SettingsDataStoreProvider.kt
│ │ │ │ │ │ ├── source
│ │ │ │ │ │ │ └── VersionList.kt
│ │ │ │ │ │ └── SettingsKeys.kt
│ │ │ │ │ └── remote
│ │ │ │ │ │ ├── dto
│ │ │ │ │ │ ├── GithubAssetDto.kt
│ │ │ │ │ │ └── GithubReleaseDto.kt
│ │ │ │ │ │ ├── mapper
│ │ │ │ │ │ └── GithubReleaseMapper.kt
│ │ │ │ │ │ ├── api
│ │ │ │ │ │ └── GithubApi.kt
│ │ │ │ │ │ └── repository
│ │ │ │ │ │ └── UpdateRepositoryImpl.kt
│ │ │ │ └── presentation
│ │ │ │ │ ├── event
│ │ │ │ │ └── SettingsUiEvent.kt
│ │ │ │ │ ├── page
│ │ │ │ │ ├── changelog
│ │ │ │ │ │ └── viewmodel
│ │ │ │ │ │ │ └── ChangelogViewModel.kt
│ │ │ │ │ ├── customisation
│ │ │ │ │ │ └── viewmodel
│ │ │ │ │ │ │ └── CustomisationViewModel.kt
│ │ │ │ │ ├── autoupdate
│ │ │ │ │ │ └── viewmodel
│ │ │ │ │ │ │ └── AutoUpdateViewModel.kt
│ │ │ │ │ └── lookandfeel
│ │ │ │ │ │ └── viewmodel
│ │ │ │ │ │ └── LookAndFeelViewModel.kt
│ │ │ │ │ ├── components
│ │ │ │ │ ├── shape
│ │ │ │ │ │ └── CardCornerShape.kt
│ │ │ │ │ ├── switch
│ │ │ │ │ │ └── SettingsSwitch.kt
│ │ │ │ │ ├── item
│ │ │ │ │ │ ├── PreferenceItemView.kt
│ │ │ │ │ │ └── ChangelogItemLayout.kt
│ │ │ │ │ └── image
│ │ │ │ │ │ └── ProfilePic.kt
│ │ │ │ │ └── provider
│ │ │ │ │ └── RadioGroupOptionsProvider.kt
│ │ │ │ ├── App.kt
│ │ │ │ ├── navigation
│ │ │ │ ├── LocalNavController.kt
│ │ │ │ └── NavTransitions.kt
│ │ │ │ ├── calender
│ │ │ │ ├── domain
│ │ │ │ │ └── usecase
│ │ │ │ │ │ └── GetWeekDayLabelsUseCase.kt
│ │ │ │ └── presentation
│ │ │ │ │ ├── components
│ │ │ │ │ ├── text
│ │ │ │ │ │ └── MonthYearHeader.kt
│ │ │ │ │ ├── bottomsheet
│ │ │ │ │ │ └── SubjectAttendanceDataBottomSheet.kt
│ │ │ │ │ ├── color
│ │ │ │ │ │ └── AttendanceColors.kt
│ │ │ │ │ ├── canvas
│ │ │ │ │ │ ├── WeekDayLabels.kt
│ │ │ │ │ │ └── MonthYearPicker.kt
│ │ │ │ │ └── modifiers
│ │ │ │ │ │ └── Modifiers.kt
│ │ │ │ │ └── image
│ │ │ │ │ └── UndrawDatePicker.kt
│ │ │ │ ├── notification
│ │ │ │ ├── worker
│ │ │ │ │ ├── AttendanceReminderWorker.kt
│ │ │ │ │ ├── UpdateCheckWorker.kt
│ │ │ │ │ └── MissedAttendanceAlertWorker.kt
│ │ │ │ ├── NotificationUtils.kt
│ │ │ │ ├── NotificationSetup.kt
│ │ │ │ └── helper
│ │ │ │ │ └── NotificationHelper.kt
│ │ │ │ ├── home
│ │ │ │ └── presentation
│ │ │ │ │ └── components
│ │ │ │ │ ├── text
│ │ │ │ │ └── SubjectText.kt
│ │ │ │ │ ├── image
│ │ │ │ │ └── UndrawRelaxedReading.kt
│ │ │ │ │ └── label
│ │ │ │ │ └── Label.kt
│ │ │ │ └── MainActivity.kt
│ │ ├── assets
│ │ │ └── translators.json
│ │ └── AndroidManifest.xml
│ ├── test
│ │ └── java
│ │ │ └── in
│ │ │ └── hridayan
│ │ │ └── driftly
│ │ │ └── ExampleUnitTest.kt
│ └── androidTest
│ │ └── java
│ │ └── in
│ │ └── hridayan
│ │ └── driftly
│ │ └── ExampleInstrumentedTest.kt
└── proguard-rules.pro
├── fastlane
└── metadata
│ └── android
│ └── en-US
│ ├── title.txt
│ ├── changelogs
│ ├── 12.txt
│ ├── 11.txt
│ ├── 7.txt
│ ├── 10.txt
│ ├── 6.txt
│ ├── 2.txt
│ ├── 3.txt
│ ├── 4.txt
│ ├── 1.txt
│ ├── 5.txt
│ └── 8.txt
│ ├── short_description.txt
│ ├── images
│ ├── icon.png
│ ├── featureGraphic.png
│ └── phoneScreenshots
│ │ ├── 1.png
│ │ ├── 2.png
│ │ ├── 3.png
│ │ ├── 4.png
│ │ ├── 5.png
│ │ └── 6.png
│ └── full_description.txt
├── assets
├── izzy.png
├── github.png
└── telegram.png
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
├── crowdin.yml
├── settings.gradle.kts
├── .github
├── FUNDING.yml
├── workflows
│ ├── build_debug.yml
│ ├── crowdin.yml
│ ├── crowdin_upload.yml
│ ├── update_translation_progress.yml
│ ├── crowdin_download.yml
│ └── update_issue_templates.yml
├── scripts
│ ├── update_bug_report_template.py
│ └── update_crash_report_template.py
└── ISSUE_TEMPLATE
│ ├── feature_request.yml
│ ├── crash_report.yml
│ └── bug_report.yml
├── gradle.properties
└── docs
├── translations-dark.svg
└── translations-light.svg
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/title.txt:
--------------------------------------------------------------------------------
1 | Driftly
--------------------------------------------------------------------------------
/app/src/main/res/resources.properties:
--------------------------------------------------------------------------------
1 | unqualifiedResLocale=en
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/12.txt:
--------------------------------------------------------------------------------
1 |
2 | * Fixed some UI margins
--------------------------------------------------------------------------------
/assets/izzy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DP-Hridayan/Driftly/HEAD/assets/izzy.png
--------------------------------------------------------------------------------
/assets/github.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DP-Hridayan/Driftly/HEAD/assets/github.png
--------------------------------------------------------------------------------
/assets/telegram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DP-Hridayan/Driftly/HEAD/assets/telegram.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/11.txt:
--------------------------------------------------------------------------------
1 |
2 | * Fixed card colors in dynamic theming
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/short_description.txt:
--------------------------------------------------------------------------------
1 | A material 3 designed attendance tracker app
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/7.txt:
--------------------------------------------------------------------------------
1 |
2 | * Fixed an issue where restoring backup failed in some devices.
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DP-Hridayan/Driftly/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/res/font/one_ui_sans_400.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DP-Hridayan/Driftly/HEAD/app/src/main/res/font/one_ui_sans_400.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/one_ui_sans_600.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DP-Hridayan/Driftly/HEAD/app/src/main/res/font/one_ui_sans_600.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/one_ui_sans_700.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DP-Hridayan/Driftly/HEAD/app/src/main/res/font/one_ui_sans_700.ttf
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/10.txt:
--------------------------------------------------------------------------------
1 |
2 | * Revamped whole settings UI
3 |
4 | * Added background cards to settings items
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/dp_hridayan.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DP-Hridayan/Driftly/HEAD/app/src/main/res/mipmap-hdpi/dp_hridayan.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DP-Hridayan/Driftly/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DP-Hridayan/Driftly/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/raw/spinning_gears.lottie:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DP-Hridayan/Driftly/HEAD/app/src/main/res/raw/spinning_gears.lottie
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DP-Hridayan/Driftly/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DP-Hridayan/Driftly/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DP-Hridayan/Driftly/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DP-Hridayan/Driftly/HEAD/fastlane/metadata/android/en-US/images/icon.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DP-Hridayan/Driftly/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DP-Hridayan/Driftly/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DP-Hridayan/Driftly/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DP-Hridayan/Driftly/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DP-Hridayan/Driftly/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DP-Hridayan/Driftly/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DP-Hridayan/Driftly/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DP-Hridayan/Driftly/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DP-Hridayan/Driftly/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DP-Hridayan/Driftly/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DP-Hridayan/Driftly/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DP-Hridayan/Driftly/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DP-Hridayan/Driftly/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DP-Hridayan/Driftly/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DP-Hridayan/Driftly/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/featureGraphic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DP-Hridayan/Driftly/HEAD/fastlane/metadata/android/en-US/images/featureGraphic.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DP-Hridayan/Driftly/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DP-Hridayan/Driftly/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DP-Hridayan/Driftly/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DP-Hridayan/Driftly/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DP-Hridayan/Driftly/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DP-Hridayan/Driftly/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | .idea
5 | .kotlin
6 | .DS_Store
7 | /build
8 | /captures
9 | .externalNativeBuild
10 | .cxx
11 | local.properties
12 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/6.txt:
--------------------------------------------------------------------------------
1 |
2 | * Added option to backup and restore app data locally
3 |
4 | * Localised hardcoded strings
5 |
6 | * Minor ui improvements
7 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/core/domain/model/StreakType.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.core.domain.model
2 |
3 | enum class StreakType {
4 | START, MIDDLE, END, NONE
5 | }
--------------------------------------------------------------------------------
/app/src/main/res/xml/file_paths.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/2.txt:
--------------------------------------------------------------------------------
1 |
2 | * Fixed some color values in color palette selection
3 |
4 | * Fixed some UI margins
5 |
6 | * Added prerelease build update checker
7 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/core/presentation/components/svg/_DrawableVectors.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.core.presentation.components.svg
2 |
3 | public object DynamicColorImageVectors
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/settings/domain/model/SettingsType.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.settings.domain.model
2 |
3 | enum class SettingsType {
4 | Switch, SwitchBanner, RadioGroup, None
5 | }
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/3.txt:
--------------------------------------------------------------------------------
1 |
2 | * Improved color scheme in high contrast dark theme
3 |
4 | * Added what\'s new bottomsheet to show changelogs after an update
5 |
6 | * Code optimisations
7 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/App.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly
2 |
3 | import android.app.Application
4 | import dagger.hilt.android.HiltAndroidApp
5 |
6 | @HiltAndroidApp
7 | class App : Application()
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/core/domain/model/GithubReleaseType.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.core.domain.model
2 |
3 | object GithubReleaseType {
4 | const val STABLE = 1
5 | const val PRE_RELEASE = 2
6 | }
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/core/domain/model/SubjectCardStyle.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.core.domain.model
2 |
3 | object SubjectCardStyle {
4 | const val CARD_STYLE_A = 1
5 | const val CARD_STYLE_B = 2
6 | }
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/settings/domain/model/ChangelogItem.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.settings.domain.model
2 |
3 | data class ChangelogItem(
4 | val versionName: String,
5 | val changelog: String
6 | )
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/settings/domain/model/RadioButtonOptions.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.settings.domain.model
2 |
3 | data class RadioButtonOptions(
4 | val value: Int,
5 | val labelResId: Int
6 | )
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/settings/domain/model/GithubRelease.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.settings.domain.model
2 |
3 | data class GitHubRelease(
4 | val tagName: String,
5 | val apkUrl: String? = null
6 | )
7 |
--------------------------------------------------------------------------------
/crowdin.yml:
--------------------------------------------------------------------------------
1 | project_id_env: CROWDIN_PROJECT_ID
2 | api_token_env: CROWDIN_PERSONAL_TOKEN
3 | files:
4 | - source: '**/src/main/res/values/strings.xml'
5 | translation: '**/src/main/res/values-%android_code%/%original_file_name%'
6 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/settings/domain/model/BackupOption.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.settings.domain.model
2 |
3 | enum class BackupOption {
4 | SETTINGS_ONLY,
5 | DATABASE_ONLY,
6 | SETTINGS_AND_DATABASE
7 | }
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/4.txt:
--------------------------------------------------------------------------------
1 |
2 | * Added behavior settings to modify certain app behavior
3 |
4 | * Added option to remember the last viewed month
5 |
6 | * Added option to turn off attendance streak visuals in calendar
7 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/core/domain/model/TotalAttendance.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.core.domain.model
2 |
3 | data class TotalAttendance(
4 | val totalPresent: Int = 0,
5 | val totalAbsent: Int = 0,
6 | val totalCount: Int = 0
7 | )
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/core/di/qualifiers/ApiHttpClient.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.core.di.qualifiers
2 |
3 | import jakarta.inject.Qualifier
4 |
5 | @Qualifier
6 | @Retention(AnnotationRetention.BINARY)
7 | annotation class ApiHttpClient
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/core/domain/model/AttendanceStatus.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.core.domain.model
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | enum class AttendanceStatus {
7 | PRESENT, ABSENT, UNMARKED
8 | }
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/core/domain/model/SubjectAttendance.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.core.domain.model
2 |
3 | data class SubjectAttendance(
4 | val presentCount: Int = 0,
5 | val absentCount: Int = 0,
6 | val totalCount: Int = 0
7 | )
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1.txt:
--------------------------------------------------------------------------------
1 |
2 | * Initial release
3 |
4 | * Added calendar to mark attendance status
5 |
6 | * Added per subject progress display
7 |
8 | * Added ability to add, remove and edit subjects
9 |
10 | * Added settings for customising user experience
11 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/settings/domain/model/CustomFontFamily.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.settings.domain.model
2 |
3 | object CustomFontFamily {
4 | const val SYSTEM_FONT = 0
5 | const val ONE_UI_SANS = 1
6 | const val SANS_SERIF = 2
7 | const val MONOSPACE = 3
8 | }
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/settings/domain/model/NotificationState.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.settings.domain.model
2 |
3 | data class NotificationState(
4 | val enableNotifications: Boolean,
5 | val markAttendance: Boolean,
6 | val missedAttendance: Boolean,
7 | val updateAvailable: Boolean
8 | )
9 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/settings/domain/repository/UpdateRepository.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.settings.domain.repository
2 |
3 | import `in`.hridayan.driftly.settings.domain.model.UpdateResult
4 |
5 | interface UpdateRepository {
6 | suspend fun fetchLatestRelease(includePrerelease:Boolean): UpdateResult
7 | }
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/settings/data/local/provider/SettingsDataStoreProvider.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.settings.data.local.provider
2 |
3 | import android.content.Context
4 | import androidx.datastore.preferences.preferencesDataStore
5 |
6 | val Context.settingsDataStore by preferencesDataStore(name = "settings")
7 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/core/domain/model/SubjectError.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.core.domain.model
2 |
3 | sealed class SubjectError {
4 | object Empty : SubjectError()
5 | object AlreadyExists : SubjectError()
6 | data class Unknown(val message: String) : SubjectError()
7 | object None : SubjectError()
8 | }
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/navigation/LocalNavController.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.navigation
2 |
3 | import androidx.compose.runtime.staticCompositionLocalOf
4 | import androidx.navigation.NavHostController
5 |
6 | val LocalNavController =
7 | staticCompositionLocalOf { error("No LocalNavController provided") }
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/5.txt:
--------------------------------------------------------------------------------
1 |
2 | * Switched some UI components to material expressive theme
3 |
4 | * Added option to make Monday as first day of the week in the Calendar
5 |
6 | * Added option to directly download the latest updated apk inside the app without the need to visit the Github repository
7 |
8 | * Fixed many UI inconsistencies
9 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/8.txt:
--------------------------------------------------------------------------------
1 |
2 | * Added notification alerts for various events like attendance reminder, missed attendance alert and notification alert when new version of the app is detected
3 |
4 | * Improved the batch selection of subject cards in home screen with support to dismiss selection by back press
5 |
6 | * Fixed system bars colors in android 11 and below
7 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/core/domain/model/NotificationTags.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.core.domain.model
2 |
3 | object NotificationTags {
4 | const val REMINDER_TO_MARK_ATTENDANCE = "reminder_to_mark_attendance"
5 | const val NOTIFY_WHEN_MISSED_ATTENDANCE = "notify_when_missed_attendance"
6 | const val NOTIFY_WHEN_UPDATE_AVAILABLE = "notify_when_update_available"
7 | }
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/settings/domain/model/UpdateResult.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.settings.domain.model
2 |
3 | sealed class UpdateResult {
4 | data class Success(val release: GitHubRelease, val isUpdateAvailable: Boolean) : UpdateResult()
5 | object NetworkError : UpdateResult()
6 | object Timeout : UpdateResult()
7 | object UnknownError : UpdateResult()
8 | }
9 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionSha256Sum=8fad3d78296ca518113f3d29016617c7f9367dc005f932bd9d93bf45ba46072b
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip
5 | networkTimeout=10000
6 | validateDistributionUrl=true
7 | zipStoreBase=GRADLE_USER_HOME
8 | zipStorePath=wrapper/dists
9 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/settings/data/remote/dto/GithubAssetDto.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.settings.data.remote.dto
2 |
3 | import kotlinx.serialization.SerialName
4 | import kotlinx.serialization.Serializable
5 |
6 | @Serializable
7 | data class GitHubAssetDto(
8 | @SerialName("name")
9 | val name: String,
10 |
11 | @SerialName("browser_download_url")
12 | val browserDownloadUrl: String
13 | )
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/core/domain/repository/DownloadRepository.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.core.domain.repository
2 |
3 | import `in`.hridayan.driftly.core.domain.model.DownloadState
4 |
5 | interface DownloadRepository {
6 | suspend fun downloadApk(
7 | url: String,
8 | fileName: String,
9 | onProgress: (DownloadState) -> Unit
10 | )
11 |
12 | fun cancelDownload()
13 | }
14 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/settings/data/local/source/VersionList.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.settings.data.local.source
2 |
3 | val versionList = listOf(
4 | "v1.9.0",
5 | "v1.8.3",
6 | "v1.8.2",
7 | "v1.8.1",
8 | "v1.8.0",
9 | "v1.7.0",
10 | "v1.6.0",
11 | "v1.5.1",
12 | "v1.5.0",
13 | "v1.4.0",
14 | "v1.3.0",
15 | "v1.2.0",
16 | "v1.1.0",
17 | "v1.0.0"
18 | )
19 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/settings/data/remote/dto/GithubReleaseDto.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.settings.data.remote.dto
2 |
3 | import kotlinx.serialization.SerialName
4 | import kotlinx.serialization.Serializable
5 |
6 | @Serializable
7 | data class GitHubReleaseDto(
8 | @SerialName("tag_name")
9 | val tagName: String,
10 |
11 | @SerialName("assets")
12 | val assets: List = emptyList()
13 | )
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/settings/domain/usecase/ToggleSettingUseCase.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.settings.domain.usecase
2 |
3 | import `in`.hridayan.driftly.settings.data.local.SettingsKeys
4 | import `in`.hridayan.driftly.settings.domain.repository.SettingsRepository
5 |
6 | class ToggleSettingUseCase(private val repo: SettingsRepository) {
7 | suspend operator fun invoke(key: SettingsKeys) = repo.toggleSetting(key)
8 | }
--------------------------------------------------------------------------------
/app/src/test/java/in/hridayan/driftly/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/calender/domain/usecase/GetWeekDayLabelsUseCase.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.calender.domain.usecase
2 |
3 | class GetWeekDayLabelsUseCase {
4 |
5 | operator fun invoke(isMondayFirstDay: Boolean): List {
6 | return if (isMondayFirstDay) {
7 | listOf("M", "T", "W", "T", "F", "S", "S")
8 | } else {
9 | listOf("S", "M", "T", "W", "T", "F", "S")
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/core/data/model/SubjectEntity.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.core.data.model
2 |
3 | import androidx.room.Entity
4 | import androidx.room.PrimaryKey
5 | import kotlinx.serialization.Serializable
6 |
7 | @Serializable
8 | @Entity(tableName = "subjects")
9 | data class SubjectEntity(
10 | @PrimaryKey(autoGenerate = true) val id:Int = 0,
11 | val subject:String,
12 | val savedMonth: Int? = null,
13 | val savedYear: Int? = null
14 | )
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/settings/domain/repository/BackupAndRestoreRepository.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.settings.domain.repository
2 |
3 | import android.net.Uri
4 | import `in`.hridayan.driftly.settings.domain.model.BackupOption
5 |
6 | interface BackupAndRestoreRepository {
7 | suspend fun backupDataToFile(uri: Uri, option: BackupOption): Boolean
8 | suspend fun restoreDataFromFile(uri: Uri): Boolean
9 | suspend fun getBackupTimeFromFile(uri: Uri): String?
10 | }
11 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/core/domain/model/DownloadState.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.core.domain.model
2 |
3 | import java.io.File
4 |
5 | sealed class DownloadState {
6 | object Idle : DownloadState()
7 | object Started : DownloadState()
8 | data class Progress(val percent: Float) : DownloadState()
9 | data class Success(val file: File) : DownloadState()
10 | data class Error(val message: String) : DownloadState()
11 | object Cancelled : DownloadState()
12 | }
13 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/core/data/database/Migrations.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.core.data.database
2 |
3 | import androidx.room.migration.Migration
4 | import androidx.sqlite.db.SupportSQLiteDatabase
5 |
6 | val MIGRATION_2_3 = object : Migration(2, 3) {
7 | override fun migrate(db: SupportSQLiteDatabase) {
8 | db.execSQL("ALTER TABLE subjects ADD COLUMN savedMonth INTEGER")
9 | db.execSQL("ALTER TABLE subjects ADD COLUMN savedYear INTEGER")
10 | }
11 | }
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/core/data/database/Converters.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.core.data.database
2 |
3 | import androidx.room.TypeConverter
4 | import `in`.hridayan.driftly.core.domain.model.AttendanceStatus
5 |
6 | object Converters {
7 | @TypeConverter
8 | @JvmStatic
9 | fun fromStatus(value: AttendanceStatus): String = value.name
10 |
11 | @TypeConverter
12 | @JvmStatic
13 | fun toStatus(value: String): AttendanceStatus = AttendanceStatus.valueOf(value)
14 | }
15 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/core/data/model/AttendanceEntity.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.core.data.model
2 |
3 | import androidx.room.Entity
4 | import `in`.hridayan.driftly.core.domain.model.AttendanceStatus
5 | import kotlinx.serialization.Serializable
6 |
7 | @Serializable
8 | @Entity(
9 | tableName = "attendance",
10 | primaryKeys = ["subjectId", "date"]
11 | )
12 | data class AttendanceEntity(
13 | val subjectId: Int,
14 | val date: String,
15 | val status: AttendanceStatus = AttendanceStatus.UNMARKED
16 | )
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/settings/data/remote/mapper/GithubReleaseMapper.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.settings.data.remote.mapper
2 |
3 | import `in`.hridayan.driftly.settings.data.remote.dto.GitHubReleaseDto
4 | import `in`.hridayan.driftly.settings.domain.model.GitHubRelease
5 |
6 | fun GitHubReleaseDto.toDomain(): GitHubRelease {
7 | val apkAsset = assets.firstOrNull { it.name.endsWith(".apk") }
8 | return GitHubRelease(
9 | tagName = tagName,
10 | apkUrl = apkAsset?.browserDownloadUrl
11 | )
12 | }
13 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/full_description.txt:
--------------------------------------------------------------------------------
1 | Driftly is a fully-featured Attendance Tracker app with Material Design 3 UI, letting you save your daily attendance and analyze them, packed in a beautiful and smart interface
2 |
3 | Features
4 |
5 | - 🎨 Modern Material Design 3 UI
6 |
7 | - 📲 Save attendance
8 |
9 | - 🕵️Progress summarization
10 |
11 | - 🗓️ Inbuilt Calendar
12 |
13 | - 🛠️ Support for Adding, Deleting or Editing
14 |
15 | - 🌑 AMOLED-Friendly Dark Theme
16 |
17 | - 🎉 Packed with Extras
18 |
19 |
20 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/core/utils/HapticUtils.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.core.utils
2 |
3 | import android.view.HapticFeedbackConstants
4 | import android.view.View
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.platform.LocalView
7 |
8 | object HapticUtils {
9 |
10 | fun View.weakHaptic() {
11 | performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK)
12 | }
13 |
14 | fun View.strongHaptic() {
15 | performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
16 | }
17 | }
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/settings/domain/model/BackupData.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.settings.domain.model
2 |
3 | import `in`.hridayan.driftly.core.data.model.AttendanceEntity
4 | import `in`.hridayan.driftly.core.data.model.SubjectEntity
5 | import kotlinx.serialization.Serializable
6 |
7 | @Serializable
8 | data class BackupData(
9 | val settings: Map? = null,
10 | val attendance: List? = null,
11 | val subjects: List? = null,
12 | val backupTime: String
13 | )
14 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_version.xml:
--------------------------------------------------------------------------------
1 |
8 |
11 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/core/di/entry/WorkerEntryPoint.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.core.di.entry
2 |
3 | import dagger.hilt.EntryPoint
4 | import dagger.hilt.InstallIn
5 | import dagger.hilt.components.SingletonComponent
6 | import `in`.hridayan.driftly.core.domain.repository.AttendanceRepository
7 | import `in`.hridayan.driftly.settings.domain.usecase.CheckUpdateUseCase
8 |
9 | @EntryPoint
10 | @InstallIn(SingletonComponent::class)
11 | interface WorkerEntryPoint {
12 | fun checkUpdateUseCase(): CheckUpdateUseCase
13 | fun attendanceRepository(): AttendanceRepository
14 | }
15 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/settings/domain/model/PreferenceGroup.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.settings.domain.model
2 |
3 | import androidx.annotation.StringRes
4 | import `in`.hridayan.driftly.settings.domain.model.PreferenceItem
5 |
6 | sealed class PreferenceGroup {
7 | data class Category(
8 | @param:StringRes val categoryNameResId: Int,
9 | val items: List
10 | ) : PreferenceGroup()
11 |
12 | data class Items(val items: List) : PreferenceGroup()
13 | object HorizontalDivider : PreferenceGroup()
14 | data class CustomComposable(val label: String) : PreferenceGroup()
15 | }
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/core/domain/provider/SeedColorProvider.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.core.domain.provider
2 |
3 | import `in`.hridayan.driftly.core.presentation.provider.AppSeedColors
4 | import `in`.hridayan.driftly.core.presentation.provider.SeedColor
5 |
6 | object SeedColorProvider {
7 | val seed = AppSeedColors.Color05.colors
8 |
9 | var primary: Int = seed.primary
10 | var secondary: Int = seed.secondary
11 | var tertiary: Int = seed.tertiary
12 |
13 | fun setSeedColor(seed: SeedColor) {
14 | primary = seed.primary
15 | secondary = seed.secondary
16 | tertiary = seed.tertiary
17 | }
18 | }
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google {
4 | content {
5 | includeGroupByRegex("com\\.android.*")
6 | includeGroupByRegex("com\\.google.*")
7 | includeGroupByRegex("androidx.*")
8 | }
9 | }
10 | mavenCentral()
11 | gradlePluginPortal()
12 | }
13 | }
14 |
15 | @Suppress("UnstableApiUsage")
16 | dependencyResolutionManagement {
17 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
18 | repositories {
19 | google()
20 | mavenCentral()
21 | }
22 | }
23 |
24 | rootProject.name = "Driftly"
25 | include(":app")
26 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_mail.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/core/domain/usecase/DownloadApkUseCase.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.core.domain.usecase
2 |
3 | import `in`.hridayan.driftly.core.domain.model.DownloadState
4 | import `in`.hridayan.driftly.core.domain.repository.DownloadRepository
5 | import javax.inject.Inject
6 |
7 | class DownloadApkUseCase @Inject constructor(
8 | private val repo: DownloadRepository
9 | ) {
10 | suspend operator fun invoke(
11 | url: String,
12 | fileName: String,
13 | onProgress: (DownloadState) -> Unit
14 | ) {
15 | repo.downloadApk(url, fileName, onProgress)
16 | }
17 |
18 | fun cancel() {
19 | repo.cancelDownload()
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/core/utils/ApkInstaller.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.core.utils
2 |
3 | import android.app.Activity
4 | import android.content.Intent
5 | import androidx.core.content.FileProvider
6 | import java.io.File
7 |
8 | fun Activity.installApk(file: File) {
9 | val apkUri = FileProvider.getUriForFile(
10 | this,
11 | "$packageName.fileprovider",
12 | file
13 | )
14 |
15 | val intent = Intent(Intent.ACTION_VIEW).apply {
16 | setDataAndType(apkUri, "application/vnd.android.package-archive")
17 | addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
18 | }
19 |
20 | startActivity(intent)
21 | }
22 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/core/data/database/SubjectDatabase.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.core.data.database
2 |
3 |
4 | import androidx.room.Database
5 | import androidx.room.RoomDatabase
6 | import androidx.room.TypeConverters
7 | import `in`.hridayan.driftly.core.data.model.AttendanceEntity
8 | import `in`.hridayan.driftly.core.data.model.SubjectEntity
9 |
10 | @Database(
11 | entities = [SubjectEntity::class, AttendanceEntity::class],
12 | version = 3,
13 | exportSchema = false
14 | )
15 | @TypeConverters(Converters::class)
16 | abstract class SubjectDatabase : RoomDatabase() {
17 | abstract fun subjectDao(): SubjectDao
18 | abstract fun attendanceDao(): AttendanceDao
19 | }
20 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/calender/presentation/components/text/MonthYearHeader.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.calender.presentation.components.text
2 |
3 | import androidx.compose.animation.animateContentSize
4 | import androidx.compose.material3.MaterialTheme
5 | import androidx.compose.material3.Text
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.Modifier
8 |
9 | @Composable
10 | fun MonthYearHeader(
11 | modifier: Modifier = Modifier,
12 | month: String, year: Int
13 | ) {
14 | Text(
15 | text = "$month, $year",
16 | style = MaterialTheme.typography.headlineLarge,
17 | modifier = modifier
18 | .animateContentSize()
19 | )
20 | }
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/core/utils/FileUtils.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.core.utils
2 |
3 | import android.content.Context
4 | import android.database.Cursor
5 | import android.net.Uri
6 | import android.provider.OpenableColumns
7 |
8 | fun getFileNameFromUri(context: Context, uri: Uri): String? {
9 | return context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
10 | val nameIndex = cursor.getColumnIndexOpenableColumnName()
11 | cursor.moveToFirst()
12 | if (nameIndex != -1) cursor.getString(nameIndex) else null
13 | }
14 | }
15 |
16 | fun Cursor.getColumnIndexOpenableColumnName(): Int {
17 | return getColumnIndex(OpenableColumns.DISPLAY_NAME)
18 | }
19 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_format_italic.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_license.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_telegram.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/in/hridayan/driftly/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("in.hridayan.driftly", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_format_clear.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_format_bold.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_format_underline.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/core/domain/repository/SubjectRepository.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.core.domain.repository
2 |
3 | import `in`.hridayan.driftly.core.data.model.SubjectEntity
4 | import kotlinx.coroutines.flow.Flow
5 |
6 | interface SubjectRepository {
7 | fun getAllSubjects(): Flow>
8 | suspend fun getAllSubjectsOnce(): List
9 | fun getSubjectById(id: Int): Flow
10 | suspend fun insertSubject(subject: SubjectEntity)
11 | suspend fun insertAllSubjects(subjects: List)
12 | suspend fun updateSubject(subjectId: Int, newName: String)
13 | suspend fun deleteSubject(subjectId: Int)
14 | suspend fun deleteAllSubjects()
15 | fun getSubjectCount(): Flow
16 | fun isSubjectExists(subject: String): Flow
17 | suspend fun updateSavedMonthYear(subjectId: Int, month: Int, year: Int)
18 | }
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: DP-Hridayan
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
12 | polar: # Replace with a single Polar username
13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
14 | thanks_dev: # Replace with a single thanks.dev username
15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
16 |
--------------------------------------------------------------------------------
/.github/workflows/build_debug.yml:
--------------------------------------------------------------------------------
1 | name: Build Debug APK
2 |
3 | on:
4 | workflow_dispatch:
5 | pull_request:
6 |
7 | jobs:
8 | build:
9 | name: Build Debug APK
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - name: Checkout code
14 | uses: actions/checkout@v3
15 |
16 | - name: Set up Java
17 | uses: actions/setup-java@v3
18 | with:
19 | distribution: 'zulu'
20 | java-version: '21'
21 |
22 | - name: Set up Android SDK
23 | uses: android-actions/setup-android@v3
24 |
25 | - name: Grant execute permission to Gradle
26 | run: chmod +x ./gradlew
27 |
28 | - name: Build Debug APK
29 | run: ./gradlew assembleDebug
30 |
31 | - name: Upload Debug APK to Artifacts
32 | uses: actions/upload-artifact@v4
33 | with:
34 | name: debug-apk
35 | path: app/build/outputs/apk/debug/*.apk
36 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/settings/domain/model/SettingsState.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.settings.domain.model
2 |
3 | import `in`.hridayan.driftly.core.presentation.provider.SeedColor
4 |
5 | data class SettingsState(
6 | val isAutoUpdate: Boolean,
7 | val themeMode: Int,
8 | val isHighContrastDarkMode: Boolean,
9 | val seedColor: SeedColor,
10 | val isDynamicColor: Boolean,
11 | val isHapticEnabled: Boolean,
12 | val subjectCardCornerRadius: Float,
13 | val subjectCardStyle: Int,
14 | val githubReleaseType: Int,
15 | val savedVersionCode: Int,
16 | val showAttendanceStreaks: Boolean,
17 | val rememberCalendarMonthYear: Boolean,
18 | val startWeekOnMonday: Boolean,
19 | val enableDirectDownload: Boolean,
20 | val notificationPreference: Boolean,
21 | val notificationPermissionDialogShown: Boolean,
22 | val showGithubWarningDialog: Boolean,
23 | val fontFamily: Int
24 | )
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/core/presentation/theme/Shape.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.core.presentation.theme
2 |
3 | import androidx.compose.foundation.shape.RoundedCornerShape
4 | import androidx.compose.ui.unit.dp
5 |
6 | object Shape {
7 | val labelStroke = 1.dp
8 |
9 | val cardStrokeSmall = 1.dp
10 | val cardStrokeMedium = 2.dp
11 | val cardStrokeLarge = 4.dp
12 |
13 | val cardCornerSmall = RoundedCornerShape(8.dp)
14 | val cardCornerMedium = RoundedCornerShape(16.dp)
15 | val cardCornerLarge = RoundedCornerShape(25.dp)
16 |
17 | val cardTopCornersRounded = RoundedCornerShape(
18 | topStart = 16.dp,
19 | topEnd = 16.dp,
20 | bottomStart = 2.dp,
21 | bottomEnd = 2.dp
22 | )
23 |
24 | val cardBottomCornersRounded = RoundedCornerShape(
25 | topStart = 2.dp,
26 | topEnd = 2.dp,
27 | bottomStart = 16.dp,
28 | bottomEnd = 16.dp
29 | )
30 | }
--------------------------------------------------------------------------------
/.github/workflows/crowdin.yml:
--------------------------------------------------------------------------------
1 | name: Crowdin Synchronization
2 |
3 | on:
4 | workflow_dispatch:
5 |
6 | permissions:
7 | contents: write
8 | pull-requests: write
9 |
10 | jobs:
11 |
12 | synchronize-with-crowdin:
13 | name: Synchronize with Crowdin
14 | if: github.repository_owner == 'DP-Hridayan'
15 | runs-on: ubuntu-latest
16 | steps:
17 |
18 | - name: Checkout
19 | uses: actions/checkout@v4
20 |
21 | - name: Sync Translations
22 | uses: crowdin/github-action@v2
23 | with:
24 | upload_translations: false
25 | upload_sources: true
26 | download_translations: true
27 | localization_branch_name: localization
28 | create_pull_request: true
29 | env:
30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
31 | CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
32 | CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
33 |
--------------------------------------------------------------------------------
/.github/workflows/crowdin_upload.yml:
--------------------------------------------------------------------------------
1 | name: Crowdin Upload
2 |
3 | on:
4 | push:
5 | branches: [ "master" ]
6 | paths:
7 | - 'app/src/main/res/values/strings.xml'
8 | workflow_dispatch:
9 |
10 | jobs:
11 | synchronize-with-crowdin:
12 | name: Upload source to Crowdin
13 | if: github.repository_owner == 'DP-Hridayan'
14 | runs-on: ubuntu-latest
15 | steps:
16 |
17 | - name: Checkout
18 | uses: actions/checkout@v4
19 |
20 | - name: Upload Strings
21 | uses: crowdin/github-action@v2
22 | with:
23 | upload_translations: false
24 | upload_sources: true
25 | download_translations: false
26 | localization_branch_name: localization
27 | create_pull_request: false
28 | env:
29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
30 | CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
31 | CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
32 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/core/presentation/provider/FontProvider.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.core.presentation.provider
2 |
3 | import androidx.compose.ui.text.font.Font
4 | import androidx.compose.ui.text.font.FontFamily
5 | import androidx.compose.ui.text.font.FontWeight
6 | import `in`.hridayan.driftly.R
7 | import `in`.hridayan.driftly.settings.domain.model.CustomFontFamily
8 |
9 | fun getFontFamily(fontFamily: Int): FontFamily? {
10 | return when (fontFamily) {
11 | CustomFontFamily.ONE_UI_SANS -> oneUiSans
12 |
13 | CustomFontFamily.SANS_SERIF -> FontFamily.SansSerif
14 |
15 | CustomFontFamily.MONOSPACE -> FontFamily.Monospace
16 |
17 | else -> FontFamily.Default
18 | }
19 | }
20 |
21 | val oneUiSans = FontFamily(
22 | Font(resId = R.font.one_ui_sans_700, weight = FontWeight.Bold),
23 | Font(resId = R.font.one_ui_sans_600, weight = FontWeight.SemiBold),
24 | Font(resId = R.font.one_ui_sans_400, weight = FontWeight.Normal),
25 | )
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/settings/data/remote/api/GithubApi.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.settings.data.remote.api
2 |
3 | import `in`.hridayan.driftly.settings.data.remote.dto.GitHubReleaseDto
4 | import io.ktor.client.HttpClient
5 | import io.ktor.client.call.body
6 | import io.ktor.client.request.get
7 | import javax.inject.Inject
8 |
9 | class GitHubApi @Inject constructor(
10 | private val client: HttpClient
11 | ) {
12 | private val releasesUrl = "https://api.github.com/repos/DP-Hridayan/Driftly/releases"
13 | private val latestReleaseUrl = "https://api.github.com/repos/DP-Hridayan/Driftly/releases/latest"
14 |
15 | suspend fun fetchLatestRelease(includePrerelease: Boolean): GitHubReleaseDto {
16 | return if (includePrerelease) {
17 | val allReleases: List = client.get(releasesUrl).body()
18 | allReleases.first()
19 | } else {
20 | client.get(latestReleaseUrl).body()
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/settings/presentation/event/SettingsUiEvent.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.settings.presentation.event
2 |
3 | import android.content.Intent
4 | import `in`.hridayan.driftly.settings.data.local.SettingsKeys
5 | import `in`.hridayan.driftly.settings.domain.model.BackupOption
6 |
7 | sealed class SettingsUiEvent {
8 | data class ShowToast(val message: String) : SettingsUiEvent()
9 | data class Navigate(val route: Any) : SettingsUiEvent()
10 | data class ShowDialog(val key : SettingsKeys) : SettingsUiEvent()
11 | data class ShowBottomSheet(val key : SettingsKeys) : SettingsUiEvent()
12 | data class OpenUrl(val url:String) : SettingsUiEvent()
13 | data class LaunchIntent(val intent: Intent) : SettingsUiEvent()
14 | data class RequestPermission(val permission: String) : SettingsUiEvent()
15 |
16 | data class RequestDocumentUriForBackup(val backupOption: BackupOption) : SettingsUiEvent()
17 | object RequestDocumentUriForRestore : SettingsUiEvent()
18 | }
19 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/notification/worker/AttendanceReminderWorker.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.notification.worker
2 |
3 | import android.Manifest
4 | import android.content.Context
5 | import androidx.annotation.RequiresPermission
6 | import androidx.work.CoroutineWorker
7 | import androidx.work.WorkerParameters
8 | import dagger.hilt.android.qualifiers.ApplicationContext
9 | import `in`.hridayan.driftly.notification.NotificationSetup
10 |
11 | class AttendanceReminderWorker(
12 | @param:ApplicationContext private val context: Context,
13 | workerParams: WorkerParameters
14 | ) : CoroutineWorker(context, workerParams) {
15 |
16 | @RequiresPermission(Manifest.permission.POST_NOTIFICATIONS)
17 | override suspend fun doWork(): Result {
18 | return try {
19 | NotificationSetup.showAttendanceReminderNotification(applicationContext)
20 | Result.success()
21 | } catch (e: Exception) {
22 | e.printStackTrace()
23 | Result.retry()
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
23 | -keep class androidx.datastore.preferences.PreferencesProto$* { *; }
24 | -keepclassmembers class androidx.datastore.preferences.PreferencesProto$* { *; }
25 | -keep class org.slf4j.** { *; }
26 | -dontwarn org.slf4j.**
27 |
28 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/core/common/constants/UrlConst.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.core.common.constants
2 |
3 | object UrlConst {
4 | const val URL_GITHUB_REPO = "https://github.com/DP-Hridayan/Driftly"
5 | const val URL_GITHUB_ISSUE_REPORT =
6 | "https://github.com/DP-Hridayan/Driftly/issues/new?template=bug_report.yml"
7 | const val URL_GITHUB_ISSUE_FEATURE_REQUEST =
8 | "https://github.com/DP-Hridayan/Driftly/issues/new?template=feature_request.yml"
9 | const val URL_GITHUB_REPO_LICENSE =
10 | "https://github.com/DP-Hridayan/Driftly/blob/master/LICENSE.md"
11 | const val URL_DEV_TELEGRAM = "https://t.me/hridayan"
12 | const val URL_DEV_GITHUB = "https://github.com/DP-Hridayan"
13 | const val URL_DEV_EMAIL = "mailto:hridayanofficial@gmail.com"
14 | const val URL_DEV_BM_COFFEE = "https://www.buymeacoffee.com/hridayan"
15 | const val URL_EMAIL_BUG = "mailto:hridayanofficial@gmail.com?subject=Bug%20Report"
16 | const val URL_EMAIL_FEATURE = "mailto:hridayanofficial@gmail.com?subject=Feature%20Suggestion"
17 | }
18 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/home/presentation/components/text/SubjectText.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.home.presentation.components.text
2 |
3 | import androidx.compose.animation.animateContentSize
4 | import androidx.compose.animation.core.FastOutSlowInEasing
5 | import androidx.compose.animation.core.tween
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.material3.Text
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.graphics.Color
11 |
12 | @Composable
13 | fun SubjectText(
14 | modifier: Modifier = Modifier,
15 | subject: String,
16 | subjectTextColor: Color = MaterialTheme.colorScheme.onSurface
17 | ) {
18 | Text(
19 | text = subject,
20 | modifier = modifier
21 | .animateContentSize(
22 | animationSpec = tween(
23 | durationMillis = 500, easing = FastOutSlowInEasing
24 | )
25 | ),
26 | style = MaterialTheme.typography.titleMedium,
27 | color = subjectTextColor
28 | )
29 | }
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/core/presentation/components/progress/LoadingSpinner.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.core.presentation.components.progress
2 |
3 | import androidx.compose.foundation.layout.Box
4 | import androidx.compose.foundation.layout.padding
5 | import androidx.compose.material3.CircularProgressIndicator
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.Alignment
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.graphics.Color
11 | import androidx.compose.ui.unit.Dp
12 | import androidx.compose.ui.unit.dp
13 |
14 | @Composable
15 | fun LoadingSpinner(
16 | modifier: Modifier = Modifier,
17 | strokeWidth: Dp = 3.dp,
18 | color: Color = MaterialTheme.colorScheme.onPrimaryContainer
19 | ) {
20 | Box(
21 | modifier = modifier,
22 | contentAlignment = Alignment.Center
23 | ) {
24 | CircularProgressIndicator(
25 | modifier = Modifier.padding(2.dp),
26 | strokeWidth = strokeWidth,
27 | color = color
28 | )
29 | }
30 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_check_circle.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/settings/domain/usecase/GetAllChangelogsUseCase.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.settings.domain.usecase
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Context
5 | import `in`.hridayan.driftly.R
6 | import `in`.hridayan.driftly.settings.domain.model.ChangelogItem
7 | import `in`.hridayan.driftly.settings.data.local.source.versionList
8 |
9 | class GetAllChangelogsUseCase(
10 | private val context: Context,
11 | private val versions: List = versionList
12 | ) {
13 | @SuppressLint("DiscouragedApi")
14 | operator fun invoke(): List {
15 | val res = context.resources
16 | val pkg = context.packageName
17 |
18 | return versions.map { version ->
19 | val resourceName = "changelogs_" + version.replace('.', '_')
20 |
21 | val resId = res.getIdentifier(resourceName, "string", pkg)
22 |
23 | val text = context.getString(
24 | if (resId != 0) resId else R.string.no_changelog_found
25 | )
26 |
27 | ChangelogItem(versionName = version, changelog = text)
28 | }
29 | }
30 | }
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/core/di/UseCaseModule.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.core.di
2 |
3 | import android.content.Context
4 | import dagger.Module
5 | import dagger.Provides
6 | import dagger.hilt.InstallIn
7 | import dagger.hilt.android.qualifiers.ApplicationContext
8 | import dagger.hilt.components.SingletonComponent
9 | import `in`.hridayan.driftly.calender.domain.usecase.GetWeekDayLabelsUseCase
10 | import `in`.hridayan.driftly.core.domain.repository.DownloadRepository
11 | import `in`.hridayan.driftly.core.domain.usecase.DownloadApkUseCase
12 | import `in`.hridayan.driftly.settings.domain.usecase.GetAllChangelogsUseCase
13 |
14 | @Module
15 | @InstallIn(SingletonComponent::class)
16 | object UseCaseModule {
17 | @Provides
18 | fun provideGetWeekDayLabelsUseCase(): GetWeekDayLabelsUseCase =
19 | GetWeekDayLabelsUseCase()
20 |
21 | @Provides
22 | fun provideGetChangelogsUseCase(@ApplicationContext context: Context): GetAllChangelogsUseCase =
23 | GetAllChangelogsUseCase(context)
24 |
25 | @Provides
26 | fun provideDownloadApkUseCase(repo: DownloadRepository): DownloadApkUseCase =
27 | DownloadApkUseCase(repo)
28 | }
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/core/utils/PaletteExtensions.kt:
--------------------------------------------------------------------------------
1 | @file:SuppressLint("RestrictedApi")
2 |
3 | package `in`.hridayan.driftly.core.utils
4 |
5 | import android.annotation.SuppressLint
6 | import androidx.compose.ui.graphics.Color
7 | import com.google.android.material.color.utilities.CorePalette
8 | import `in`.hridayan.driftly.core.domain.provider.SeedColorProvider
9 |
10 | private val primaryPalette get() = CorePalette.of(SeedColorProvider.primary)
11 | private val secondaryPalette get() = CorePalette.of(SeedColorProvider.secondary)
12 | private val tertiaryPalette get() = CorePalette.of(SeedColorProvider.tertiary)
13 |
14 | val Int.a1 get() = primaryPalette.a1.getHct(this.toDouble()).toInt().let { Color(it) }
15 | val Int.a2 get() = secondaryPalette.a1.getHct(this.toDouble()).toInt().let { Color(it) }
16 | val Int.a3 get() = tertiaryPalette.a1.getHct(this.toDouble()).toInt().let { Color(it) }
17 | val Int.n1 get() = primaryPalette.n1.getHct(this.toDouble()).toInt().let { Color(it) }
18 | val Int.n2 get() = primaryPalette.n2.getHct(this.toDouble()).toInt().let { Color(it) }
19 | val Int.error get() = primaryPalette.error.getHct(this.toDouble()).toInt().let { Color(it) }
20 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_font_download.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_info.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_add_comment.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_github.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/calender/presentation/components/bottomsheet/SubjectAttendanceDataBottomSheet.kt:
--------------------------------------------------------------------------------
1 | @file:OptIn(ExperimentalMaterial3Api::class)
2 |
3 | package `in`.hridayan.driftly.calender.presentation.components.bottomsheet
4 |
5 | import androidx.compose.foundation.layout.fillMaxWidth
6 | import androidx.compose.material3.ExperimentalMaterial3Api
7 | import androidx.compose.material3.ModalBottomSheet
8 | import androidx.compose.material3.rememberModalBottomSheetState
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.ui.Modifier
11 | import `in`.hridayan.driftly.calender.presentation.components.card.AttendanceCardWithTabs
12 |
13 | @Composable
14 | fun SubjectAttendanceDataBottomSheet(
15 | modifier: Modifier = Modifier,
16 | onDismiss: () -> Unit = {},
17 | subjectId: Int = 0
18 | ) {
19 | val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
20 |
21 | ModalBottomSheet(
22 | modifier = modifier,
23 | sheetState = sheetState,
24 | onDismissRequest = onDismiss
25 | ) {
26 | AttendanceCardWithTabs(
27 | modifier = Modifier.fillMaxWidth(),
28 | subjectId = subjectId
29 | )
30 | }
31 | }
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/calender/presentation/components/color/AttendanceColors.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.calender.presentation.components.color
2 |
3 | import androidx.compose.material3.MaterialTheme
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.ui.graphics.Color
6 | import `in`.hridayan.driftly.core.domain.model.AttendanceStatus
7 |
8 | data class AttendanceColors(
9 | val background: Color,
10 | val foreground: Color
11 | )
12 |
13 | @Composable
14 | fun getAttendanceColors(status: AttendanceStatus): AttendanceColors {
15 | return when (status) {
16 | AttendanceStatus.PRESENT -> AttendanceColors(
17 | background = MaterialTheme.colorScheme.primary,
18 | foreground = MaterialTheme.colorScheme.primaryContainer
19 | )
20 | AttendanceStatus.ABSENT -> AttendanceColors(
21 | background = MaterialTheme.colorScheme.error,
22 | foreground = MaterialTheme.colorScheme.errorContainer
23 | )
24 | AttendanceStatus.UNMARKED -> AttendanceColors(
25 | background = MaterialTheme.colorScheme.surface,
26 | foreground = MaterialTheme.colorScheme.onSurface
27 | )
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/settings/presentation/page/changelog/viewmodel/ChangelogViewModel.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.settings.presentation.page.changelog.viewmodel
2 |
3 | import androidx.compose.runtime.State
4 | import androidx.compose.runtime.mutableStateOf
5 | import androidx.lifecycle.ViewModel
6 | import androidx.lifecycle.viewModelScope
7 | import dagger.hilt.android.lifecycle.HiltViewModel
8 | import `in`.hridayan.driftly.settings.domain.model.ChangelogItem
9 | import `in`.hridayan.driftly.settings.domain.usecase.GetAllChangelogsUseCase
10 | import kotlinx.coroutines.launch
11 | import javax.inject.Inject
12 |
13 |
14 | @HiltViewModel
15 | class ChangelogViewModel @Inject constructor(
16 | private val getAllChangelogsUseCase: GetAllChangelogsUseCase
17 | ) : ViewModel() {
18 |
19 | private val _changelogs = mutableStateOf>(emptyList())
20 | val changelogs: State> = _changelogs
21 |
22 | init {
23 | viewModelScope.launch {
24 | _changelogs.value = getAllChangelogsUseCase()
25 | }
26 | }
27 |
28 | fun splitStringToLines(input: String): List {
29 | return input.split("\n").filter { it.isNotBlank() }
30 | }
31 | }
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/settings/presentation/components/shape/CardCornerShape.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.settings.presentation.components.shape
2 |
3 | import androidx.compose.foundation.shape.RoundedCornerShape
4 | import androidx.compose.ui.unit.dp
5 |
6 | object CardCornerShape {
7 | val SINGLE_CARD = RoundedCornerShape(16.dp)
8 |
9 | val FIRST_CARD = RoundedCornerShape(
10 | topStart = 16.dp,
11 | topEnd = 16.dp,
12 | bottomStart = 4.dp,
13 | bottomEnd = 4.dp
14 | )
15 |
16 | val MIDDLE_CARD = RoundedCornerShape(
17 | topStart = 4.dp,
18 | topEnd = 4.dp,
19 | bottomStart = 4.dp,
20 | bottomEnd = 4.dp
21 | )
22 |
23 | val LAST_CARD = RoundedCornerShape(
24 | topStart = 4.dp,
25 | topEnd = 4.dp,
26 | bottomStart = 16.dp,
27 | bottomEnd = 16.dp
28 | )
29 |
30 | fun getRoundedShape(
31 | index: Int,
32 | size: Int,
33 | ): RoundedCornerShape {
34 | return when {
35 | size == 1 -> SINGLE_CARD
36 |
37 | index == 0 -> FIRST_CARD
38 |
39 | index == size - 1 -> LAST_CARD
40 |
41 | else -> MIDDLE_CARD
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_matchcase.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_handyman.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_report.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_database.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/core/utils/Utils.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.core.utils
2 |
3 | import android.app.Activity
4 | import android.content.ActivityNotFoundException
5 | import android.content.Context
6 | import android.content.Intent
7 | import android.net.ConnectivityManager
8 | import android.net.NetworkCapabilities
9 | import android.widget.Toast
10 | import androidx.core.net.toUri
11 |
12 | fun openUrl(url: String, context: Context) {
13 | try {
14 | val intent = Intent(Intent.ACTION_VIEW, url.toUri())
15 |
16 | if (context !is Activity) {
17 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
18 | }
19 |
20 | context.startActivity(intent)
21 | } catch (ignored: ActivityNotFoundException) {
22 | }
23 | }
24 |
25 | fun isNetworkAvailable(context: Context): Boolean {
26 | val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
27 | val network = cm.activeNetwork ?: return false
28 | val actNw = cm.getNetworkCapabilities(network) ?: return false
29 | return actNw.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
30 | }
31 |
32 | fun showToast(context: Context, message: String) {
33 | Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
34 | }
35 |
36 |
37 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/notification/NotificationUtils.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.notification
2 |
3 | import android.Manifest
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.content.pm.PackageManager
7 | import android.os.Build
8 | import android.provider.Settings
9 | import androidx.core.app.NotificationManagerCompat
10 | import androidx.core.content.ContextCompat
11 |
12 | fun isNotificationPermissionGranted(context: Context): Boolean {
13 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
14 | val hasPermission = ContextCompat.checkSelfPermission(
15 | context,
16 | Manifest.permission.POST_NOTIFICATIONS
17 | ) == PackageManager.PERMISSION_GRANTED
18 |
19 | return hasPermission && NotificationManagerCompat.from(context).areNotificationsEnabled()
20 | } else {
21 | return NotificationManagerCompat.from(context).areNotificationsEnabled()
22 | }
23 | }
24 |
25 | fun createAppNotificationSettingsIntent(context: Context): Intent {
26 | return Intent().apply {
27 | action = Settings.ACTION_APP_NOTIFICATION_SETTINGS
28 | putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
29 | flags = Intent.FLAG_ACTIVITY_NEW_TASK
30 | }
31 | }
32 |
33 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/core/di/DatabaseModule.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.core.di
2 |
3 | import android.content.Context
4 | import androidx.room.Room
5 | import dagger.Module
6 | import dagger.Provides
7 | import dagger.hilt.InstallIn
8 | import dagger.hilt.android.qualifiers.ApplicationContext
9 | import dagger.hilt.components.SingletonComponent
10 | import `in`.hridayan.driftly.core.data.database.AttendanceDao
11 | import `in`.hridayan.driftly.core.data.database.MIGRATION_2_3
12 | import `in`.hridayan.driftly.core.data.database.SubjectDao
13 | import `in`.hridayan.driftly.core.data.database.SubjectDatabase
14 | import javax.inject.Singleton
15 |
16 | @Module
17 | @InstallIn(SingletonComponent::class)
18 | object DatabaseModule {
19 |
20 | @Provides
21 | @Singleton
22 | fun provideDatabase(@ApplicationContext context: Context): SubjectDatabase =
23 | Room.databaseBuilder(
24 | context,
25 | SubjectDatabase::class.java,
26 | "attendance_app_db"
27 | )
28 | .addMigrations(MIGRATION_2_3)
29 | .build()
30 |
31 | @Provides
32 | fun provideSubjectDao(db: SubjectDatabase): SubjectDao = db.subjectDao()
33 |
34 | @Provides
35 | fun provideAttendanceDao(db: SubjectDatabase): AttendanceDao = db.attendanceDao()
36 | }
37 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_notifications_important.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/core/presentation/components/tooltip/TooltipContent.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.core.presentation.components.tooltip
2 |
3 | import androidx.compose.material3.ExperimentalMaterial3Api
4 | import androidx.compose.material3.PlainTooltip
5 | import androidx.compose.material3.Text
6 | import androidx.compose.material3.TooltipAnchorPosition
7 | import androidx.compose.material3.TooltipBox
8 | import androidx.compose.material3.TooltipDefaults
9 | import androidx.compose.material3.rememberTooltipState
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.runtime.getValue
12 | import androidx.compose.runtime.mutableStateOf
13 | import androidx.compose.runtime.remember
14 | import androidx.compose.runtime.setValue
15 |
16 | @OptIn(ExperimentalMaterial3Api::class)
17 | @Composable
18 | fun TooltipContent(text: String, content: @Composable () -> Unit) {
19 | var isTooltipVisible by remember { mutableStateOf(false) }
20 |
21 | TooltipBox(
22 | positionProvider = TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
23 | tooltip = {
24 | PlainTooltip {
25 | Text(text = text)
26 | }
27 | },
28 | state = rememberTooltipState(isTooltipVisible)
29 | ) {
30 | content()
31 | }
32 | }
--------------------------------------------------------------------------------
/.github/scripts/update_bug_report_template.py:
--------------------------------------------------------------------------------
1 | import subprocess
2 | import yaml
3 |
4 | template_path = ".github/ISSUE_TEMPLATE/bug_report.yml"
5 |
6 | try:
7 | tags = subprocess.check_output(["git", "tag", "--sort=-v:refname"]).decode().splitlines()
8 | except subprocess.CalledProcessError:
9 | tags = []
10 |
11 | if not tags:
12 | tags = ["v1.0.0 (Initial release)"]
13 |
14 | latest_tag = tags[0]
15 |
16 | version_dropdown = {
17 | "type": "dropdown",
18 | "id": "version",
19 | "attributes": {
20 | "label": "Version",
21 | "description": "What version of Driftly are you running?",
22 | "options": [f"{tag} (Latest)" if tag == latest_tag else tag for tag in tags],
23 | "default": 0
24 | },
25 | "validations": {
26 | "required": True
27 | }
28 | }
29 |
30 | with open(template_path, "r") as f:
31 | template = yaml.safe_load(f)
32 |
33 | if "body" in template:
34 | for i, field in enumerate(template["body"]):
35 | if isinstance(field, dict) and field.get("type") == "dropdown" and field.get(
36 | "id") == "version":
37 | template["body"][i] = version_dropdown
38 | break
39 |
40 | with open(template_path, "w") as f:
41 | yaml.dump(template, f, sort_keys=False)
42 |
43 | print("Dropdown updated with tags:", tags)
44 |
--------------------------------------------------------------------------------
/.github/scripts/update_crash_report_template.py:
--------------------------------------------------------------------------------
1 | import subprocess
2 | import yaml
3 |
4 | template_path = ".github/ISSUE_TEMPLATE/crash_report.yml"
5 |
6 | try:
7 | tags = subprocess.check_output(["git", "tag", "--sort=-v:refname"]).decode().splitlines()
8 | except subprocess.CalledProcessError:
9 | tags = []
10 |
11 | if not tags:
12 | tags = ["v1.0.0 (Initial release)"]
13 |
14 | latest_tag = tags[0]
15 |
16 | version_dropdown = {
17 | "type": "dropdown",
18 | "id": "version",
19 | "attributes": {
20 | "label": "Version",
21 | "description": "What version of Driftly are you running?",
22 | "options": [f"{tag} (Latest)" if tag == latest_tag else tag for tag in tags],
23 | "default": 0
24 | },
25 | "validations": {
26 | "required": True
27 | }
28 | }
29 |
30 | with open(template_path, "r") as f:
31 | template = yaml.safe_load(f)
32 |
33 | if "body" in template:
34 | for i, field in enumerate(template["body"]):
35 | if isinstance(field, dict) and field.get("type") == "dropdown" and field.get(
36 | "id") == "version":
37 | template["body"][i] = version_dropdown
38 | break
39 |
40 | with open(template_path, "w") as f:
41 | yaml.dump(template, f, sort_keys=False)
42 |
43 | print("Dropdown updated with tags:", tags)
44 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/settings/presentation/components/switch/SettingsSwitch.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.settings.presentation.components.switch
2 |
3 | import androidx.compose.foundation.layout.size
4 | import androidx.compose.material.icons.Icons
5 | import androidx.compose.material.icons.rounded.Check
6 | import androidx.compose.material.icons.rounded.Close
7 | import androidx.compose.material3.Icon
8 | import androidx.compose.material3.Switch
9 | import androidx.compose.material3.SwitchDefaults
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.ui.Modifier
12 |
13 | @Composable
14 | fun SettingsSwitch(
15 | modifier: Modifier = Modifier,
16 | checked: Boolean,
17 | onCheckedChange: (Boolean) -> Unit,
18 | enabled: Boolean = true,
19 | ) {
20 | Switch(
21 | checked = checked,
22 | enabled = enabled,
23 | onCheckedChange = {
24 | onCheckedChange(it)
25 | },
26 | thumbContent = {
27 | val thumbIcon = if (checked) Icons.Rounded.Check else Icons.Rounded.Close
28 |
29 | Icon(
30 | imageVector = thumbIcon,
31 | contentDescription = null,
32 | modifier = Modifier.size(SwitchDefaults.IconSize)
33 | )
34 | },
35 | modifier = modifier
36 | )
37 | }
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/core/domain/repository/AttendanceRepository.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.core.domain.repository
2 |
3 | import `in`.hridayan.driftly.core.data.model.AttendanceEntity
4 | import `in`.hridayan.driftly.core.domain.model.AttendanceStatus
5 | import kotlinx.coroutines.flow.Flow
6 |
7 | interface AttendanceRepository {
8 | suspend fun insertAttendance(attendance: AttendanceEntity)
9 | suspend fun insertAllAttendances(attendances: List)
10 | suspend fun updateAttendance(attendance: AttendanceEntity)
11 | suspend fun deleteAttendance(subjectId: Int, date: String)
12 | suspend fun deleteAllAttendances()
13 | suspend fun deleteAllAttendanceForSubject(subjectId: Int)
14 | suspend fun getAllAttendancesOnce(): List
15 | suspend fun hasUnmarkedAttendanceForDate(date: String): Boolean
16 | fun getAttendanceForSubject(subjectId: Int): Flow>
17 | fun getTotalCountByStatus(status: AttendanceStatus): Flow
18 | fun getCountBySubjectAndStatus(subjectId: Int, status: AttendanceStatus): Flow
19 | fun getPresentCountForMonth(subjectId: Int, year: Int, month: Int): Flow
20 | fun getAbsentCountForMonth(subjectId: Int, year: Int, month: Int): Flow
21 | fun getTotalCountForMonth(subjectId: Int, year: Int, month: Int): Flow
22 | }
23 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_upload_file.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/core/di/RepositoryModule.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.core.di
2 |
3 | import dagger.Binds
4 | import dagger.Module
5 | import dagger.hilt.InstallIn
6 | import dagger.hilt.components.SingletonComponent
7 | import `in`.hridayan.driftly.core.data.repository.AttendanceRepositoryImpl
8 | import `in`.hridayan.driftly.core.data.repository.SubjectRepositoryImpl
9 | import `in`.hridayan.driftly.core.domain.repository.AttendanceRepository
10 | import `in`.hridayan.driftly.core.domain.repository.SubjectRepository
11 | import `in`.hridayan.driftly.settings.data.local.repository.BackupAndRestoreRepositoryImpl
12 | import `in`.hridayan.driftly.settings.domain.repository.BackupAndRestoreRepository
13 | import javax.inject.Singleton
14 |
15 | @Module
16 | @InstallIn(SingletonComponent::class)
17 | abstract class RepositoryModule {
18 | @Binds
19 | @Singleton
20 | abstract fun bindSubjectRepository(
21 | subjectRepositoryImpl: SubjectRepositoryImpl
22 | ): SubjectRepository
23 |
24 | @Binds
25 | @Singleton
26 | abstract fun bindAttendanceRepository(
27 | attendanceRepositoryImpl: AttendanceRepositoryImpl
28 | ): AttendanceRepository
29 |
30 | @Binds
31 | @Singleton
32 | abstract fun bindBackupAndRestoreRepository(
33 | backupAndRestoreRepositoryImpl: BackupAndRestoreRepositoryImpl
34 | ): BackupAndRestoreRepository
35 | }
36 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_notifications.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/core/presentation/components/progress/CircularProgressWithText.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.core.presentation.components.progress
2 |
3 | import android.annotation.SuppressLint
4 | import androidx.compose.foundation.layout.Box
5 | import androidx.compose.material3.MaterialTheme
6 | import androidx.compose.material3.Text
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.Alignment
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.graphics.lerp
11 | import androidx.compose.ui.unit.sp
12 |
13 | @SuppressLint("DefaultLocale")
14 | @Composable
15 | fun CircularProgressWithText(modifier: Modifier = Modifier, progress: Float) {
16 | val progressText = "${String.format("%.0f", progress * 100)}%"
17 | val progressColor = lerp(
18 | start = MaterialTheme.colorScheme.error,
19 | stop = MaterialTheme.colorScheme.primary,
20 | fraction = progress.coerceIn(0f, 1f)
21 | )
22 |
23 | Box(contentAlignment = Alignment.Center, modifier = modifier) {
24 | AnimatedCircularProgressIndicator(
25 | progress = progress, animationDuration = 3000
26 | )
27 | Text(
28 | text = progressText,
29 | color = progressColor,
30 | style = MaterialTheme.typography.bodySmall.copy(
31 | fontSize = 10.sp
32 | )
33 | )
34 | }
35 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_restore_page.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/settings/presentation/components/item/PreferenceItemView.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.settings.presentation.components.item
2 |
3 | import androidx.compose.foundation.shape.RoundedCornerShape
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.ui.Modifier
6 | import androidx.compose.ui.unit.dp
7 | import `in`.hridayan.driftly.settings.domain.model.PreferenceItem
8 |
9 | @Composable
10 | fun PreferenceItemView(
11 | item: PreferenceItem,
12 | modifier: Modifier = Modifier,
13 | roundedShape: RoundedCornerShape = RoundedCornerShape(16.dp)
14 | ) {
15 | when (item) {
16 | is PreferenceItem.BoolPreferenceItem -> BooleanPreferenceItemView(
17 | item = item,
18 | modifier = modifier,
19 | roundedShape = roundedShape
20 | )
21 |
22 | is PreferenceItem.IntPreferenceItem -> IntPreferenceItemView(
23 | item = item,
24 | modifier = modifier
25 | )
26 |
27 | // is PreferenceItem.StringPreferenceItem -> StringPreferenceItemView(item)
28 | // is PreferenceItem.FloatPreferenceItem -> FloatPreferenceItemView(item)
29 |
30 | is PreferenceItem.NullPreferenceItem -> NullPreferenceItemView(
31 | item = item,
32 | modifier = modifier,
33 | roundedShape = roundedShape
34 | )
35 |
36 | else -> {}
37 | }
38 | }
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/core/presentation/components/button/BackButton.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.core.presentation.components.button
2 |
3 | import androidx.compose.material.icons.Icons
4 | import androidx.compose.material.icons.automirrored.rounded.ArrowBack
5 | import androidx.compose.material3.Icon
6 | import androidx.compose.material3.IconButton
7 | import androidx.compose.material3.MaterialTheme
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.res.stringResource
11 | import `in`.hridayan.driftly.R
12 | import `in`.hridayan.driftly.core.common.LocalWeakHaptic
13 | import `in`.hridayan.driftly.core.presentation.components.tooltip.TooltipContent
14 | import `in`.hridayan.driftly.navigation.LocalNavController
15 |
16 | @Composable
17 | fun BackButton(modifier: Modifier = Modifier) {
18 | val weakHaptic = LocalWeakHaptic.current
19 | val navController = LocalNavController.current
20 |
21 | TooltipContent(stringResource(R.string.back_button)) {
22 | IconButton(onClick = {
23 | weakHaptic()
24 | navController.popBackStack()
25 | }) {
26 | Icon(
27 | modifier = modifier,
28 | imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
29 | contentDescription = null,
30 | tint = MaterialTheme.colorScheme.onSurface
31 | )
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.yml:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature Request
3 | description: Suggest an idea for this project
4 | title: "[Feature request] "
5 | labels: [enhancement]
6 | assignees: []
7 |
8 | body:
9 | - type: textarea
10 | id: problem-description
11 | attributes:
12 | label: Is your feature request related to a problem? Please describe.
13 | description: A clear and concise description of what the problem is.
14 | placeholder: Ex. I'm always frustrated when [...]
15 | validations:
16 | required: true
17 |
18 | - type: textarea
19 | id: solution-description
20 | attributes:
21 | label: Describe the solution you'd like
22 | description: A clear and concise description of what you want to happen.
23 | placeholder: What solution would you propose?
24 | validations:
25 | required: true
26 |
27 | - type: textarea
28 | id: alternatives-description
29 | attributes:
30 | label: Describe alternatives you've considered
31 | description: A clear and concise description of any alternative solutions or features you've considered.
32 | placeholder: Have you considered any alternative approaches?
33 |
34 | - type: textarea
35 | id: additional-context
36 | attributes:
37 | label: Additional context
38 | description: Add any other context or screenshots about the feature request here.
39 | placeholder: Any other details or screenshots that might help?
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/core/common/LocalSettings.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.core.common
2 |
3 | import androidx.appcompat.app.AppCompatDelegate
4 | import androidx.compose.runtime.compositionLocalOf
5 | import `in`.hridayan.driftly.core.domain.model.GithubReleaseType
6 | import `in`.hridayan.driftly.core.domain.model.SubjectCardStyle
7 | import `in`.hridayan.driftly.core.domain.provider.SeedColorProvider
8 | import `in`.hridayan.driftly.settings.domain.model.CustomFontFamily
9 | import `in`.hridayan.driftly.settings.domain.model.SettingsState
10 |
11 | val LocalSettings = compositionLocalOf {
12 | SettingsState(
13 | isAutoUpdate = false,
14 | themeMode = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM,
15 | isHighContrastDarkMode = false,
16 | seedColor = SeedColorProvider.seed,
17 | isDynamicColor = true,
18 | isHapticEnabled = true,
19 | subjectCardCornerRadius = 8f,
20 | subjectCardStyle = SubjectCardStyle.CARD_STYLE_A,
21 | githubReleaseType = GithubReleaseType.STABLE,
22 | savedVersionCode = 0,
23 | showAttendanceStreaks = true,
24 | rememberCalendarMonthYear = false,
25 | startWeekOnMonday = true,
26 | enableDirectDownload = true,
27 | notificationPreference = true,
28 | notificationPermissionDialogShown = false,
29 | showGithubWarningDialog = true,
30 | fontFamily = CustomFontFamily.SYSTEM_FONT
31 | )
32 | }
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/core/di/SettingsModule.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.core.di
2 |
3 | import android.content.Context
4 | import dagger.Module
5 | import dagger.Provides
6 | import dagger.hilt.InstallIn
7 | import dagger.hilt.android.qualifiers.ApplicationContext
8 | import dagger.hilt.components.SingletonComponent
9 | import `in`.hridayan.driftly.settings.data.local.datastore.SettingsDataStore
10 | import `in`.hridayan.driftly.settings.data.local.repository.SettingsRepositoryImpl
11 | import `in`.hridayan.driftly.settings.domain.repository.SettingsRepository
12 | import `in`.hridayan.driftly.settings.domain.usecase.ToggleSettingUseCase
13 | import kotlinx.serialization.json.Json
14 | import javax.inject.Singleton
15 |
16 | @Module
17 | @InstallIn(SingletonComponent::class)
18 | object SettingsModule {
19 |
20 | @Provides
21 | fun provideSettingsDataStore(@ApplicationContext context: Context): SettingsDataStore =
22 | SettingsDataStore(context)
23 |
24 | @Provides
25 | fun provideSettingsRepository(dataStore: SettingsDataStore): SettingsRepository =
26 | SettingsRepositoryImpl(dataStore)
27 |
28 | @Provides
29 | fun provideToggleSettingUseCase(repo: SettingsRepository): ToggleSettingUseCase =
30 | ToggleSettingUseCase(repo)
31 |
32 | @Provides
33 | @Singleton
34 | fun provideJson(): Json = Json {
35 | prettyPrint = true
36 | isLenient = true
37 | ignoreUnknownKeys = true
38 | }
39 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_release_alert.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
--------------------------------------------------------------------------------
/.github/workflows/update_translation_progress.yml:
--------------------------------------------------------------------------------
1 | name: Update Translation Progress SVG
2 |
3 | on:
4 | workflow_dispatch:
5 | schedule:
6 | - cron: "0 */12 * * *"
7 |
8 | jobs:
9 | generate-svg:
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - name: Checkout code
14 | uses: actions/checkout@v3
15 | with:
16 | token: ${{ secrets.PAT_TOKEN }}
17 |
18 | - name: Set up Python
19 | uses: actions/setup-python@v4
20 | with:
21 | python-version: "3.x"
22 |
23 | - name: Install dependencies
24 | run: pip install requests
25 |
26 | - name: Generate translation SVG
27 | run: python .github/scripts/generate_crowdin_svg.py
28 | env:
29 | CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
30 | CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
31 |
32 | - name: Update README timestamp
33 | run: |
34 | TS=$(date +%s)
35 | sed -i "s/ts=[0-9]*/ts=$TS/g" README.md
36 |
37 | - name: Configure Git
38 | run: |
39 | git config user.name "Translation Bot"
40 | git config user.email "translation-bot@example.com"
41 |
42 | - name: Commit and Push
43 | run: |
44 | git add docs/translations-light.svg docs/translations-dark.svg README.md
45 | git commit -m "Update translation progress SVG and README cache-busting timestamp" || echo "No changes to commit"
46 | git push https://x-access-token:${{ secrets.PAT_TOKEN }}@github.com/${{ github.repository }} HEAD:master
47 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/settings/domain/repository/SettingsRepository.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.settings.domain.repository
2 |
3 | import `in`.hridayan.driftly.settings.data.local.SettingsKeys
4 | import `in`.hridayan.driftly.settings.domain.model.PreferenceGroup
5 | import kotlinx.coroutines.flow.Flow
6 |
7 | interface SettingsRepository {
8 |
9 | fun getBoolean(key: SettingsKeys): Flow
10 | suspend fun setBoolean(key: SettingsKeys, value: Boolean)
11 | suspend fun toggleSetting(key: SettingsKeys)
12 |
13 | fun getInt(key: SettingsKeys): Flow
14 | suspend fun setInt(key: SettingsKeys, value: Int)
15 |
16 | fun getFloat(key: SettingsKeys): Flow
17 | suspend fun setFloat(key: SettingsKeys, value: Float)
18 |
19 | fun getString(key: SettingsKeys): Flow
20 | suspend fun setString(key: SettingsKeys, value: String)
21 |
22 | suspend fun getSettingsPageList(): List
23 | suspend fun getLookAndFeelPageList(): List
24 | suspend fun getAboutPageList(): List
25 | suspend fun getAutoUpdatePageList(): List
26 | suspend fun getDarkThemePageList(): List
27 | suspend fun getBehaviorPageList(): List
28 | suspend fun getBackupPageList(): List
29 | suspend fun getNotificationsPageList(): List
30 |
31 | fun getAllDefaultSettings(): Map
32 | suspend fun getCurrentSettings(): Map
33 | suspend fun resetAndRestoreDefaults(): Boolean
34 | }
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/settings/presentation/components/image/ProfilePic.kt:
--------------------------------------------------------------------------------
1 | @file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
2 |
3 | package `in`.hridayan.driftly.settings.presentation.components.image
4 |
5 | import androidx.compose.foundation.Image
6 | import androidx.compose.foundation.border
7 | import androidx.compose.foundation.layout.size
8 | import androidx.compose.foundation.shape.CircleShape
9 | import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
10 | import androidx.compose.material3.MaterialShapes
11 | import androidx.compose.material3.MaterialTheme
12 | import androidx.compose.material3.toShape
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.draw.clip
16 | import androidx.compose.ui.graphics.painter.Painter
17 | import androidx.compose.ui.layout.ContentScale
18 | import androidx.compose.ui.unit.Dp
19 | import androidx.compose.ui.unit.dp
20 |
21 | @Composable
22 | fun ProfilePic(
23 | modifier: Modifier = Modifier,
24 | painter: Painter,
25 | size: Dp,
26 | borderWidth: Dp = 1.dp
27 | ) {
28 | Image(
29 | painter = painter,
30 | contentDescription = null,
31 | contentScale = ContentScale.Crop,
32 | modifier = modifier
33 | .size(size)
34 | .clip(MaterialShapes.Cookie9Sided.toShape())
35 | .border(
36 | width = borderWidth,
37 | color = MaterialTheme.colorScheme.primary,
38 | shape = MaterialShapes.Cookie9Sided.toShape()
39 | ),
40 | )
41 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_upcoming.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/settings/domain/usecase/CheckUpdateUseCase.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.settings.domain.usecase
2 |
3 | import `in`.hridayan.driftly.settings.domain.model.UpdateResult
4 | import `in`.hridayan.driftly.settings.domain.repository.UpdateRepository
5 | import javax.inject.Inject
6 |
7 | class CheckUpdateUseCase @Inject constructor(
8 | private val repository: UpdateRepository
9 | ) {
10 | suspend operator fun invoke(currentVersion: String, includePrerelease: Boolean): UpdateResult {
11 | return when (val result = repository.fetchLatestRelease(includePrerelease)) {
12 | is UpdateResult.Success -> {
13 | val isNewer = isNewerVersion(result.release.tagName, currentVersion)
14 | UpdateResult.Success(result.release, isNewer)
15 | }
16 |
17 | is UpdateResult.NetworkError -> UpdateResult.NetworkError
18 | is UpdateResult.Timeout -> UpdateResult.Timeout
19 | is UpdateResult.UnknownError -> UpdateResult.UnknownError
20 | }
21 | }
22 |
23 | private fun isNewerVersion(latest: String, current: String): Boolean {
24 | val latestParts = latest.trimStart('v').split(".")
25 | val currentParts = current.removeSuffix("-debug").trimStart('v').split(".")
26 |
27 | for (i in 0 until maxOf(latestParts.size, currentParts.size)) {
28 | val l = latestParts.getOrNull(i)?.toIntOrNull() ?: 0
29 | val c = currentParts.getOrNull(i)?.toIntOrNull() ?: 0
30 | if (l > c) return true
31 | if (l < c) return false
32 | }
33 | return false
34 | }
35 | }
36 |
37 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/settings/presentation/page/customisation/viewmodel/CustomisationViewModel.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.settings.presentation.page.customisation.viewmodel
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import dagger.hilt.android.lifecycle.HiltViewModel
6 | import `in`.hridayan.driftly.core.domain.model.SubjectCardStyle
7 | import `in`.hridayan.driftly.settings.data.local.SettingsKeys
8 | import `in`.hridayan.driftly.settings.domain.repository.SettingsRepository
9 | import kotlinx.coroutines.flow.SharingStarted
10 | import kotlinx.coroutines.flow.stateIn
11 | import kotlinx.coroutines.launch
12 | import javax.inject.Inject
13 |
14 | @HiltViewModel
15 | class CustomisationViewModel @Inject constructor(
16 | private val settingsRepository: SettingsRepository
17 | ) : ViewModel() {
18 | val subjectCardCornerRadius = settingsRepository.getFloat(SettingsKeys.SUBJECT_CARD_CORNER_RADIUS)
19 |
20 | fun setSubjectCardCornerRadius(cornerRadius: Float) {
21 | viewModelScope.launch {
22 | settingsRepository.setFloat(SettingsKeys.SUBJECT_CARD_CORNER_RADIUS, cornerRadius)
23 | }
24 | }
25 |
26 | val cardStyle = settingsRepository
27 | .getInt(SettingsKeys.SUBJECT_CARD_STYLE)
28 | .stateIn(
29 | scope = viewModelScope,
30 | started = SharingStarted.Eagerly,
31 | initialValue = SubjectCardStyle.CARD_STYLE_A
32 | )
33 |
34 | fun select(option: Int) {
35 | viewModelScope.launch {
36 | settingsRepository.setInt(SettingsKeys.SUBJECT_CARD_STYLE, option)
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/core/presentation/components/checkbox/CheckboxWithText.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.core.presentation.components.checkbox
2 |
3 | import androidx.compose.foundation.layout.Row
4 | import androidx.compose.foundation.layout.Spacer
5 | import androidx.compose.foundation.layout.fillMaxWidth
6 | import androidx.compose.foundation.layout.size
7 | import androidx.compose.foundation.layout.width
8 | import androidx.compose.material3.Checkbox
9 | import androidx.compose.material3.MaterialTheme
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.ui.Alignment
12 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.graphics.Color
14 | import androidx.compose.ui.unit.dp
15 | import `in`.hridayan.driftly.core.presentation.components.text.AutoResizeableText
16 |
17 | @Composable
18 | fun CheckboxWithText(
19 | modifier: Modifier = Modifier,
20 | checkedState: Boolean = false,
21 | onCheckChanged: () -> Unit,
22 | text: String,
23 | textColor: Color = MaterialTheme.colorScheme.onSurfaceVariant
24 | ) {
25 | Row(
26 | modifier = modifier.fillMaxWidth(),
27 | verticalAlignment = Alignment.CenterVertically,
28 | ) {
29 | Checkbox(
30 | modifier = Modifier.size(16.dp),
31 | checked = checkedState,
32 | onCheckedChange = { onCheckChanged() },
33 | )
34 | Spacer(modifier = Modifier.width(10.dp))
35 |
36 | AutoResizeableText(
37 | modifier = Modifier.fillMaxWidth(),
38 | text = text,
39 | style = MaterialTheme.typography.bodyLarge,
40 | color = textColor
41 | )
42 | }
43 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. For more details, visit
12 | # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
24 | android.defaults.buildfeatures.buildconfig=false
25 | android.nonFinalResIds=false
26 | kotlin.incremental=false
27 | kapt.incremental.apt=false
28 | ksp.incremental=false
29 | org.gradle.configureondemand=true
30 | org.gradle.caching=true
31 | kapt.kotlin.generated=true
32 | kapt.use.k2=true
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/calender/presentation/components/canvas/WeekDayLabels.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.calender.presentation.components.canvas
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Row
5 | import androidx.compose.foundation.layout.fillMaxWidth
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.material3.Text
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.runtime.LaunchedEffect
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.text.style.TextAlign
12 | import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
13 | import `in`.hridayan.driftly.calender.presentation.viewmodel.CalendarViewModel
14 | import `in`.hridayan.driftly.core.common.LocalSettings
15 |
16 | @Composable
17 | fun WeekDayLabels(
18 | modifier: Modifier = Modifier,
19 | calendarViewModel: CalendarViewModel = hiltViewModel()
20 | ) {
21 | val isMondayFirstDay = LocalSettings.current.startWeekOnMonday
22 |
23 | LaunchedEffect(isMondayFirstDay) {
24 | calendarViewModel.loadWeekdayLabels(isMondayFirstDay)
25 | }
26 |
27 | val labels = calendarViewModel.weekdayLabels.value
28 |
29 | Row(
30 | modifier = modifier.fillMaxWidth(),
31 | horizontalArrangement = Arrangement.SpaceEvenly
32 | ) {
33 | labels.forEach { day ->
34 | Text(
35 | text = day,
36 | style = MaterialTheme.typography.titleMedium,
37 | color = MaterialTheme.colorScheme.tertiary,
38 | modifier = Modifier.weight(1f),
39 | textAlign = TextAlign.Center
40 | )
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_reset_settings.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
--------------------------------------------------------------------------------
/.github/workflows/crowdin_download.yml:
--------------------------------------------------------------------------------
1 | name: Crowdin Download
2 |
3 | on:
4 | schedule:
5 | - cron: '0 0 * * 0'
6 | workflow_dispatch:
7 |
8 | permissions:
9 | contents: write
10 | pull-requests: write
11 |
12 | jobs:
13 |
14 | synchronize-with-crowdin:
15 | name: Download translations from Crowdin
16 | if: github.repository_owner == 'DP-Hridayan'
17 | runs-on: ubuntu-latest
18 | steps:
19 |
20 | - name: Checkout
21 | uses: actions/checkout@v4
22 |
23 | - name: Generate translators table
24 | id: "translators"
25 | uses: andrii-bodnar/action-crowdin-contributors@v2
26 | with:
27 | contributors_per_line: 6
28 | max_contributors: 500
29 | image_size: 32
30 | min_words_contributed: 1
31 | files: ./docs/translators.md
32 | crowdin_project_link: 'https://crowdin.com/project/driftly'
33 | env:
34 | CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
35 | CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
36 |
37 | - name: Save translators in assets
38 | run:
39 | printf '%s\n' '${{ steps.translators.outputs.json_report }}' > ./app/src/main/assets/translators.json
40 |
41 | - name: Download translations
42 | uses: crowdin/github-action@v2
43 | with:
44 | upload_translations: false
45 | upload_sources: false
46 | download_translations: true
47 | localization_branch_name: localization
48 | create_pull_request: true
49 | env:
50 | GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }}
51 | CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
52 | CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
53 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/notification/worker/UpdateCheckWorker.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.notification.worker
2 |
3 | import android.Manifest
4 | import android.content.Context
5 | import androidx.annotation.RequiresPermission
6 | import androidx.work.CoroutineWorker
7 | import androidx.work.WorkerParameters
8 | import dagger.hilt.android.EntryPointAccessors
9 | import dagger.hilt.android.qualifiers.ApplicationContext
10 | import `in`.hridayan.driftly.BuildConfig
11 | import `in`.hridayan.driftly.core.di.entry.WorkerEntryPoint
12 | import `in`.hridayan.driftly.notification.NotificationSetup
13 | import `in`.hridayan.driftly.settings.domain.model.UpdateResult
14 | import `in`.hridayan.driftly.settings.domain.usecase.CheckUpdateUseCase
15 |
16 | class UpdateCheckWorker(
17 | @param:ApplicationContext private val context: Context,
18 | workerParams: WorkerParameters
19 | ) : CoroutineWorker(context, workerParams) {
20 |
21 | private val checkUpdateUseCase: CheckUpdateUseCase by lazy {
22 | EntryPointAccessors.fromApplication(
23 | applicationContext,
24 | WorkerEntryPoint::class.java
25 | ).checkUpdateUseCase()
26 | }
27 |
28 | @RequiresPermission(Manifest.permission.POST_NOTIFICATIONS)
29 | override suspend fun doWork(): Result {
30 |
31 | return try {
32 | val result = checkUpdateUseCase(BuildConfig.VERSION_NAME, false)
33 |
34 | if (result is UpdateResult.Success && result.isUpdateAvailable) {
35 | NotificationSetup.showUpdateAvailableNotification(applicationContext)
36 | }
37 |
38 | Result.success()
39 | } catch (e: Exception) {
40 | e.printStackTrace()
41 | Result.retry()
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/notification/worker/MissedAttendanceAlertWorker.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.notification.worker
2 |
3 | import android.Manifest
4 | import android.content.Context
5 | import androidx.annotation.RequiresPermission
6 | import androidx.work.CoroutineWorker
7 | import androidx.work.WorkerParameters
8 | import dagger.hilt.android.EntryPointAccessors
9 | import dagger.hilt.android.qualifiers.ApplicationContext
10 | import `in`.hridayan.driftly.core.di.entry.WorkerEntryPoint
11 | import `in`.hridayan.driftly.core.domain.repository.AttendanceRepository
12 | import `in`.hridayan.driftly.notification.NotificationSetup
13 | import java.time.LocalDate
14 | import java.time.format.DateTimeFormatter
15 |
16 | class MissedAttendanceAlertWorker(
17 | @param:ApplicationContext private val context: Context,
18 | workerParams: WorkerParameters
19 | ) : CoroutineWorker(context, workerParams) {
20 |
21 | private val attendanceRepository: AttendanceRepository by lazy {
22 | EntryPointAccessors.fromApplication(
23 | applicationContext,
24 | WorkerEntryPoint::class.java
25 | ).attendanceRepository()
26 | }
27 |
28 | @RequiresPermission(Manifest.permission.POST_NOTIFICATIONS)
29 | override suspend fun doWork(): Result {
30 | return try {
31 | val today = LocalDate.now().format(DateTimeFormatter.ISO_DATE)
32 |
33 | val hasUnmarked = attendanceRepository.hasUnmarkedAttendanceForDate(today)
34 |
35 | if (hasUnmarked) {
36 | NotificationSetup.showMissedAttendanceNotification(applicationContext)
37 | }
38 |
39 | Result.success()
40 | } catch (e: Exception) {
41 | e.printStackTrace()
42 | Result.retry()
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/app/src/main/assets/translators.json:
--------------------------------------------------------------------------------
1 | [{"id":"16749463","username":"alex211952i","name":"AlexeiCrystal (alex211952i)","translated":1000,"approved":0,"picture":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16749463/medium/a5c4e3624bfd368750b8b82116a2dafb.png","languages":[{"id":"ru","name":"Russian"}]},{"id":"16090008","username":"Hussain69o","name":"Hussain (Hussain69o)","translated":875,"approved":0,"picture":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16090008/medium/cafaf425b5a021105b81001e23b51142.jpg","languages":[{"id":"ar","name":"Arabic"}]},{"id":"16675791","username":"louisgrasset","name":"Louis Grasset (louisgrasset)","translated":493,"approved":0,"picture":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16675791/medium/4f29a005a6ea0273f480c814708a7b80.png","languages":[{"id":"fr","name":"French"}]},{"id":"17127520","username":"FlyingEraser","name":"FlyingEraser","translated":489,"approved":0,"picture":"https://crowdin-static.cf-downloads.crowdin.com/avatar/17127520/medium/3c38bb435381d874e662b2db213ea683_default.png","languages":[{"id":"ja","name":"Japanese"}]},{"id":"16160370","username":"Stzyxh","name":"Stzyxh","translated":298,"approved":0,"picture":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16160370/medium/0d2594ecbcd44f3f73493c146997e524.jpeg","languages":[{"id":"de","name":"German"}]},{"id":"15483312","username":"Fuchs23","name":"Fuchs23","translated":271,"approved":0,"picture":"https://crowdin-static.cf-downloads.crowdin.com/avatar/15483312/medium/482fe0daa2e15e452e97329353eaeb26_default.png","languages":[{"id":"de","name":"German"}]},{"id":"16319000","username":"DP-Hridayan","name":"Hridayan (DP-Hridayan)","translated":31,"approved":0,"picture":"https://crowdin-static.cf-downloads.crowdin.com/avatar/16319000/medium/83750741692d1ecb16b7de139291cb30.png","languages":[{"id":"ru","name":"Russian"}]}]
2 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/core/data/database/SubjectDao.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.core.data.database
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Insert
5 | import androidx.room.OnConflictStrategy
6 | import androidx.room.Query
7 | import `in`.hridayan.driftly.core.data.model.AttendanceEntity
8 | import `in`.hridayan.driftly.core.data.model.SubjectEntity
9 | import kotlinx.coroutines.flow.Flow
10 |
11 | @Dao
12 | interface SubjectDao {
13 | @Query("SELECT * FROM subjects order by subject ASC")
14 | fun getAllSubjects(): Flow>
15 |
16 | @Query("SELECT * FROM subjects WHERE id = :id")
17 | fun getSubjectById(id: Int): Flow
18 |
19 | @Insert(onConflict = OnConflictStrategy.Companion.REPLACE)
20 | suspend fun insertSubject(subject: SubjectEntity)
21 |
22 | @Insert(onConflict = OnConflictStrategy.REPLACE)
23 | suspend fun insertAllSubjects(subjects: List)
24 |
25 | @Query("UPDATE subjects SET subject = :newName WHERE id = :subjectId")
26 | suspend fun updateSubject(subjectId: Int, newName: String)
27 |
28 | @Query("DELETE FROM subjects WHERE id = :subjectId")
29 | suspend fun deleteSubject(subjectId: Int)
30 |
31 | @Query("DELETE FROM subjects")
32 | suspend fun deleteAllSubjects()
33 |
34 | @Query("SELECT COUNT(*) FROM subjects")
35 | fun getSubjectCount(): Flow
36 |
37 | @Query("SELECT * FROM subjects ORDER BY subject ASC")
38 | suspend fun getAllSubjectsOnce(): List
39 |
40 | @Query("SELECT EXISTS(SELECT * FROM subjects WHERE subject = :subject)")
41 | fun isSubjectExists(subject: String): Flow
42 |
43 | @Query("UPDATE subjects SET savedMonth = :month, savedYear = :year WHERE id = :subjectId")
44 | suspend fun updateSavedMonthYear(subjectId: Int, month: Int, year: Int)
45 |
46 | }
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/core/utils/EncryptionHelper.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.core.utils
2 |
3 | import javax.crypto.Cipher
4 | import javax.crypto.spec.IvParameterSpec
5 | import javax.crypto.spec.SecretKeySpec
6 | import kotlin.random.Random
7 |
8 | object EncryptionHelper {
9 | private const val ALGORITHM = "AES/CBC/PKCS5Padding"
10 |
11 | private const val SECRET_KEY =
12 | "your_32_byte_secret_key_string_here!" // security is not the main goal here
13 |
14 | private val keySpec get() = normalizeKey(SECRET_KEY)
15 |
16 | fun encrypt(data: ByteArray): ByteArray {
17 | val cipher = Cipher.getInstance(ALGORITHM)
18 | val iv = Random.Default.nextBytes(16)
19 | cipher.init(Cipher.ENCRYPT_MODE, keySpec, IvParameterSpec(iv))
20 | val encrypted = cipher.doFinal(data)
21 | return iv + encrypted
22 | }
23 |
24 | fun decrypt(data: ByteArray): ByteArray {
25 | if (data.size < 16) throw IllegalArgumentException("Invalid encrypted data")
26 | val iv = data.copyOfRange(0, 16)
27 | val encryptedData = data.copyOfRange(16, data.size)
28 | val cipher = Cipher.getInstance(ALGORITHM)
29 | cipher.init(Cipher.DECRYPT_MODE, keySpec, IvParameterSpec(iv))
30 | return cipher.doFinal(encryptedData)
31 | }
32 |
33 | private fun normalizeKey(key: String, desiredLength: Int = 32): SecretKeySpec {
34 | require(desiredLength == 16 || desiredLength == 24 || desiredLength == 32) {
35 | "AES only supports 16, 24, or 32 byte keys"
36 | }
37 |
38 | val keyBytes = key.toByteArray(Charsets.UTF_8)
39 |
40 | val normalizedKey = ByteArray(desiredLength)
41 | System.arraycopy(keyBytes, 0, normalizedKey, 0, keyBytes.size.coerceAtMost(desiredLength))
42 |
43 | return SecretKeySpec(normalizedKey, "AES")
44 | }
45 | }
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/core/presentation/components/progress/AnimatedCircularProgressIndicator.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.core.presentation.components.progress
2 |
3 | import androidx.compose.animation.core.Animatable
4 | import androidx.compose.animation.core.FastOutSlowInEasing
5 | import androidx.compose.animation.core.tween
6 | import androidx.compose.material3.CircularProgressIndicator
7 | import androidx.compose.material3.MaterialTheme
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.runtime.LaunchedEffect
10 | import androidx.compose.runtime.remember
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.graphics.StrokeCap
13 | import androidx.compose.ui.graphics.lerp
14 | import androidx.compose.ui.unit.dp
15 |
16 | @Composable
17 | fun AnimatedCircularProgressIndicator(
18 | modifier: Modifier = Modifier,
19 | progress: Float ,
20 | animationDuration: Int = 1000
21 | ) {
22 | val animatedProgress = remember { Animatable(0f) }
23 |
24 | LaunchedEffect(progress) {
25 | if(!progress.isNaN())
26 | animatedProgress.animateTo(
27 | targetValue = progress.coerceIn(0f, 1f),
28 | animationSpec = tween(
29 | durationMillis = animationDuration,
30 | easing = FastOutSlowInEasing
31 | )
32 | )
33 | }
34 |
35 | val progressColor = lerp(
36 | start = MaterialTheme.colorScheme.error,
37 | stop = MaterialTheme.colorScheme.primary,
38 | fraction = animatedProgress.value
39 | )
40 |
41 | CircularProgressIndicator(
42 | progress = { animatedProgress.value },
43 | modifier = modifier,
44 | color = progressColor,
45 | strokeWidth = 3.dp,
46 | trackColor = MaterialTheme.colorScheme.secondaryContainer,
47 | strokeCap = StrokeCap.Round,
48 | )
49 | }
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/core/presentation/components/card/RoundedCornerCard.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.core.presentation.components.card
2 |
3 | import androidx.compose.foundation.BorderStroke
4 | import androidx.compose.foundation.clickable
5 | import androidx.compose.foundation.layout.ColumnScope
6 | import androidx.compose.foundation.layout.PaddingValues
7 | import androidx.compose.foundation.layout.padding
8 | import androidx.compose.foundation.shape.RoundedCornerShape
9 | import androidx.compose.material3.Card
10 | import androidx.compose.material3.CardColors
11 | import androidx.compose.material3.CardDefaults
12 | import androidx.compose.material3.CardElevation
13 | import androidx.compose.material3.MaterialTheme
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.draw.clip
17 | import androidx.compose.ui.unit.dp
18 |
19 | @Composable
20 | fun RoundedCornerCard(
21 | modifier: Modifier = Modifier,
22 | roundedCornerShape: RoundedCornerShape,
23 | colors: CardColors = CardDefaults.cardColors(
24 | containerColor = MaterialTheme.colorScheme.surfaceContainer,
25 | contentColor = MaterialTheme.colorScheme.onSurface,
26 | ),
27 | elevation: CardElevation = CardDefaults.cardElevation(),
28 | border: BorderStroke? = null,
29 | paddingValues: PaddingValues = PaddingValues(vertical = 1.dp, horizontal = 15.dp),
30 | onClick: () -> Unit = {},
31 | content: @Composable ColumnScope.() -> Unit
32 | ) {
33 | Card(
34 | modifier = modifier
35 | .padding(paddingValues)
36 | .clip(roundedCornerShape)
37 | .clickable(enabled = true, onClick = onClick),
38 | shape = roundedCornerShape,
39 | elevation = elevation,
40 | border = border,
41 | colors = colors
42 | ) {
43 | content()
44 | }
45 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
20 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
37 |
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/calender/presentation/image/UndrawDatePicker.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.calender.presentation.image
2 |
3 | import androidx.compose.animation.core.animateFloatAsState
4 | import androidx.compose.animation.core.tween
5 | import androidx.compose.foundation.Image
6 | import androidx.compose.foundation.layout.Box
7 | import androidx.compose.foundation.layout.fillMaxWidth
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.runtime.LaunchedEffect
11 | import androidx.compose.runtime.getValue
12 | import androidx.compose.runtime.mutableStateOf
13 | import androidx.compose.runtime.remember
14 | import androidx.compose.runtime.setValue
15 | import androidx.compose.ui.Alignment
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.draw.alpha
18 | import androidx.compose.ui.unit.dp
19 | import `in`.hridayan.driftly.core.presentation.components.svg.DynamicColorImageVectors
20 | import `in`.hridayan.driftly.core.presentation.components.svg.vectors.datePicker
21 |
22 | @Composable
23 | fun UndrawDatePicker(modifier: Modifier = Modifier) {
24 | var visible by remember { mutableStateOf(false) }
25 |
26 | val alpha by animateFloatAsState(
27 | targetValue = if (visible) 1f else 0f,
28 | animationSpec = tween(durationMillis = 1000),
29 | label = "alpha"
30 | )
31 |
32 | LaunchedEffect(Unit) {
33 | visible = true
34 | }
35 |
36 | Box(
37 | modifier = modifier.fillMaxWidth(),
38 | contentAlignment = Alignment.Center
39 | ) {
40 | Image(
41 | modifier = Modifier
42 | .padding(horizontal = 100.dp)
43 | .alpha(alpha),
44 | imageVector = DynamicColorImageVectors.datePicker(),
45 | contentDescription = "Empty data placeholder"
46 | )
47 | }
48 | }
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/core/di/NetworkModule.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.core.di
2 |
3 | import android.content.Context
4 | import dagger.Module
5 | import dagger.Provides
6 | import dagger.hilt.InstallIn
7 | import dagger.hilt.android.qualifiers.ApplicationContext
8 | import dagger.hilt.components.SingletonComponent
9 | import `in`.hridayan.driftly.core.data.repository.DownloadRepositoryImpl
10 | import `in`.hridayan.driftly.core.di.qualifiers.ApiHttpClient
11 | import `in`.hridayan.driftly.core.domain.repository.DownloadRepository
12 | import `in`.hridayan.driftly.settings.data.remote.api.GitHubApi
13 | import `in`.hridayan.driftly.settings.data.remote.repository.UpdateRepositoryImpl
14 | import `in`.hridayan.driftly.settings.domain.repository.UpdateRepository
15 | import io.ktor.client.HttpClient
16 | import io.ktor.client.engine.cio.CIO
17 | import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
18 | import io.ktor.serialization.kotlinx.json.json
19 | import kotlinx.serialization.json.Json
20 | import javax.inject.Singleton
21 |
22 |
23 | @Module
24 | @InstallIn(SingletonComponent::class)
25 | object NetworkModule {
26 |
27 | @Provides
28 | @Singleton
29 | @ApiHttpClient
30 | fun provideApiHttpClient(): HttpClient {
31 | return HttpClient(CIO) {
32 | install(ContentNegotiation) {
33 | json(Json { ignoreUnknownKeys = true })
34 | }
35 | }
36 | }
37 |
38 | @Provides
39 | @Singleton
40 | fun provideGitHubApi(@ApiHttpClient client: HttpClient): GitHubApi = GitHubApi(client)
41 |
42 | @Provides
43 | @Singleton
44 | fun provideUpdateRepository(api: GitHubApi): UpdateRepository = UpdateRepositoryImpl(api)
45 |
46 | @Provides
47 | @Singleton
48 | fun provideDownloadRepository(
49 | @ApplicationContext context: Context,
50 | ): DownloadRepository = DownloadRepositoryImpl(context)
51 | }
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/core/presentation/components/lottie/SpinningGearsLottie.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.core.presentation.components.lottie
2 |
3 | import androidx.compose.material3.MaterialTheme
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.LaunchedEffect
6 | import androidx.compose.runtime.getValue
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.graphics.toArgb
9 | import com.airbnb.lottie.LottieProperty
10 | import com.airbnb.lottie.compose.LottieAnimation
11 | import com.airbnb.lottie.compose.LottieCompositionSpec
12 | import com.airbnb.lottie.compose.LottieConstants
13 | import com.airbnb.lottie.compose.rememberLottieAnimatable
14 | import com.airbnb.lottie.compose.rememberLottieComposition
15 | import com.airbnb.lottie.compose.rememberLottieDynamicProperties
16 | import com.airbnb.lottie.compose.rememberLottieDynamicProperty
17 | import `in`.hridayan.driftly.R
18 |
19 | @Composable
20 | fun SpinningGearsLottie(modifier: Modifier = Modifier) {
21 | val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.spinning_gears))
22 |
23 | val color = MaterialTheme.colorScheme.onSurface
24 |
25 | val colorProperty = rememberLottieDynamicProperty(
26 | property = LottieProperty.COLOR,
27 | keyPath = arrayOf("**"),
28 | value = color.toArgb()
29 | )
30 |
31 | val dynamicProperties = rememberLottieDynamicProperties(colorProperty)
32 |
33 | val lottie = rememberLottieAnimatable()
34 |
35 | LaunchedEffect(Unit) {
36 | lottie.animate(
37 | composition = composition,
38 | iterations = LottieConstants.IterateForever
39 | )
40 | }
41 |
42 | LottieAnimation(
43 | composition = lottie.composition,
44 | progress = { lottie.progress },
45 | dynamicProperties = dynamicProperties,
46 | modifier = modifier
47 | )
48 | }
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/home/presentation/components/image/UndrawRelaxedReading.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.home.presentation.components.image
2 |
3 | import androidx.compose.animation.core.animateFloatAsState
4 | import androidx.compose.animation.core.tween
5 | import androidx.compose.foundation.Image
6 | import androidx.compose.foundation.layout.Box
7 | import androidx.compose.foundation.layout.fillMaxWidth
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.runtime.LaunchedEffect
11 | import androidx.compose.runtime.getValue
12 | import androidx.compose.runtime.mutableStateOf
13 | import androidx.compose.runtime.remember
14 | import androidx.compose.runtime.setValue
15 | import androidx.compose.ui.Alignment
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.draw.alpha
18 | import androidx.compose.ui.unit.dp
19 | import `in`.hridayan.driftly.core.presentation.components.svg.DynamicColorImageVectors
20 | import `in`.hridayan.driftly.core.presentation.components.svg.vectors.relaxedReading
21 |
22 | @Composable
23 | fun UndrawRelaxedReading(modifier: Modifier = Modifier) {
24 | var visible by remember { mutableStateOf(false) }
25 |
26 | val alpha by animateFloatAsState(
27 | targetValue = if (visible) 1f else 0f,
28 | animationSpec = tween(durationMillis = 1000),
29 | label = "alpha"
30 | )
31 |
32 | LaunchedEffect(Unit) {
33 | visible = true
34 | }
35 |
36 | Box(
37 | modifier = modifier.fillMaxWidth(),
38 | contentAlignment = Alignment.Center
39 | ) {
40 | Image(
41 | modifier = Modifier
42 | .padding(horizontal = 50.dp)
43 | .alpha(alpha),
44 | imageVector = DynamicColorImageVectors.relaxedReading(),
45 | contentDescription = "Empty data placeholder"
46 | )
47 | }
48 | }
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/navigation/NavTransitions.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.navigation
2 |
3 | import androidx.compose.animation.EnterTransition
4 | import androidx.compose.animation.ExitTransition
5 | import androidx.compose.animation.core.FastOutSlowInEasing
6 | import androidx.compose.animation.core.LinearEasing
7 | import androidx.compose.animation.core.tween
8 | import androidx.compose.animation.fadeIn
9 | import androidx.compose.animation.fadeOut
10 | import androidx.compose.animation.slideInHorizontally
11 | import androidx.compose.animation.slideOutHorizontally
12 |
13 | fun slideFadeInFromRight(): EnterTransition {
14 | return slideInHorizontally(
15 | initialOffsetX = { (it * 0.15f).toInt() },
16 | animationSpec = tween(250, easing = FastOutSlowInEasing)
17 | ) + fadeIn(
18 | initialAlpha = 0.5f,
19 | animationSpec = tween(200, delayMillis = 66, easing = LinearEasing)
20 | )
21 | }
22 |
23 | fun slideFadeOutToRight(): ExitTransition {
24 | return slideOutHorizontally(
25 | targetOffsetX = { (it * 0.10f).toInt() },
26 | animationSpec = tween(250, easing = FastOutSlowInEasing)
27 | ) + fadeOut(
28 | animationSpec = tween(50, easing = LinearEasing)
29 | )
30 | }
31 |
32 | fun slideFadeInFromLeft(): EnterTransition {
33 | return slideInHorizontally(
34 | initialOffsetX = { -(it * 0.15f).toInt() },
35 | animationSpec = tween(350, easing = FastOutSlowInEasing)
36 | ) + fadeIn(
37 | initialAlpha = 0.5f,
38 | animationSpec = tween(50, delayMillis = 66, easing = LinearEasing)
39 | )
40 | }
41 |
42 | fun slideFadeOutToLeft(): ExitTransition {
43 | return slideOutHorizontally(
44 | targetOffsetX = { -(it * 0.10f).toInt() },
45 | animationSpec = tween(250, easing = FastOutSlowInEasing)
46 | ) + fadeOut(
47 | animationSpec = tween(50, easing = LinearEasing)
48 | )
49 | }
50 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/core/data/repository/SubjectRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.core.data.repository
2 |
3 | import `in`.hridayan.driftly.core.data.database.SubjectDao
4 | import `in`.hridayan.driftly.core.data.model.SubjectEntity
5 | import `in`.hridayan.driftly.core.domain.repository.SubjectRepository
6 | import kotlinx.coroutines.flow.Flow
7 | import javax.inject.Inject
8 |
9 | class SubjectRepositoryImpl @Inject constructor(
10 | private val subjectDao: SubjectDao
11 | ) : SubjectRepository {
12 | override fun getAllSubjects(): Flow> {
13 | return subjectDao.getAllSubjects()
14 | }
15 |
16 | override fun getSubjectById(id: Int): Flow {
17 | return subjectDao.getSubjectById(id)
18 | }
19 |
20 | override suspend fun insertSubject(subject: SubjectEntity) {
21 | subjectDao.insertSubject(subject)
22 | }
23 |
24 | override suspend fun insertAllSubjects(subjects: List) {
25 | subjectDao.insertAllSubjects(subjects)
26 | }
27 |
28 | override suspend fun updateSubject(subjectId: Int, newName: String) {
29 | subjectDao.updateSubject(subjectId, newName)
30 | }
31 |
32 | override suspend fun deleteSubject(subjectId: Int) {
33 | subjectDao.deleteSubject(subjectId)
34 | }
35 |
36 | override suspend fun deleteAllSubjects() {
37 | subjectDao.deleteAllSubjects()
38 | }
39 |
40 | override suspend fun getAllSubjectsOnce(): List =
41 | subjectDao.getAllSubjectsOnce()
42 |
43 | override fun getSubjectCount(): Flow =
44 | subjectDao.getSubjectCount()
45 |
46 | override fun isSubjectExists(subject: String): Flow =
47 | subjectDao.isSubjectExists(subject)
48 |
49 | override suspend fun updateSavedMonthYear(subjectId: Int, month: Int, year: Int) {
50 | subjectDao.updateSavedMonthYear(subjectId, month, year)
51 | }
52 | }
--------------------------------------------------------------------------------
/.github/workflows/update_issue_templates.yml:
--------------------------------------------------------------------------------
1 | name: Update issue templates
2 |
3 | on:
4 | push:
5 | tags:
6 | - 'v*'
7 | delete:
8 | tags:
9 | - 'v*'
10 | workflow_dispatch:
11 |
12 | jobs:
13 | sync_dropdown:
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: actions/checkout@v3
17 | with:
18 | token: ${{ secrets.PAT_TOKEN }}
19 | fetch-depth: 0
20 |
21 | - name: Set up environment
22 | run: |
23 | if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
24 | VERSION=$(git tag --sort=-v:refname | grep '^v' | head -n 1)
25 | elif [ "${{ github.event_name }}" = "push" ]; then
26 | RAW_REF="${{ github.ref }}"
27 | VERSION="${RAW_REF##*/}"
28 | elif [ "${{ github.event_name }}" = "delete" ]; then
29 | VERSION="${GITHUB_REF##*/}"
30 | fi
31 | echo "VERSION=$VERSION" >> $GITHUB_ENV
32 | echo "EVENT=${{ github.event_name }}" >> $GITHUB_ENV
33 |
34 | - name: Configure Git
35 | run: |
36 | git config user.name "DP-Hridayan"
37 | git config user.email "hridayanofficial@gmail.com"
38 |
39 | - name: Fetch and check out master as branch
40 | run: |
41 | git fetch origin master
42 | git checkout master
43 |
44 | - name: Install dependencies
45 | run: |
46 | python3 -m pip install --upgrade pip
47 | pip install pyyaml
48 |
49 | - name: Modify bug report template using Python
50 | run: |
51 | python3 .github/scripts/update_bug_report_template.py
52 | python3 .github/scripts/update_crash_report_template.py
53 |
54 | - name: Commit and Push
55 | run: |
56 | git add .github/ISSUE_TEMPLATE/bug_report.yml
57 | git add .github/ISSUE_TEMPLATE/crash_report.yml
58 | git commit -m "Sync version dropdown: $VERSION" || echo "No changes to commit"
59 | git push origin master
60 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/settings/data/local/SettingsKeys.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.settings.data.local
2 |
3 | import androidx.appcompat.app.AppCompatDelegate
4 | import `in`.hridayan.driftly.core.domain.model.GithubReleaseType
5 | import `in`.hridayan.driftly.core.domain.model.SubjectCardStyle
6 | import `in`.hridayan.driftly.core.domain.provider.SeedColorProvider
7 | import `in`.hridayan.driftly.settings.domain.model.CustomFontFamily
8 |
9 | enum class SettingsKeys(val default: Any?) {
10 | LOOK_AND_FEEL(null),
11 | AUTO_UPDATE(false),
12 | ABOUT(null),
13 | BEHAVIOR(null),
14 | STREAK_MODIFIER(true),
15 | LANGUAGE(null),
16 | THEME_MODE(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM),
17 | DARK_THEME(null),
18 | HIGH_CONTRAST_DARK_MODE(false),
19 | PRIMARY_SEED(SeedColorProvider.primary),
20 | SECONDARY_SEED(SeedColorProvider.secondary),
21 | TERTIARY_SEED(SeedColorProvider.tertiary),
22 | DYNAMIC_COLORS(true),
23 | HAPTICS_AND_VIBRATION(true),
24 | VERSION(null),
25 | CHANGELOGS(null),
26 | REPORT(null),
27 | FEATURE_REQUEST(null),
28 | GITHUB(null),
29 | LICENSE(null),
30 | CUSTOMISATION(null),
31 | SUBJECT_CARD_CORNER_RADIUS(8f),
32 | SUBJECT_CARD_STYLE(SubjectCardStyle.CARD_STYLE_A),
33 | GITHUB_RELEASE_TYPE(GithubReleaseType.STABLE),
34 | SAVED_VERSION_CODE(0),
35 | REMEMBER_CALENDAR_MONTH_YEAR(false),
36 | START_WEEK_ON_MONDAY(false),
37 | ENABLE_DIRECT_DOWNLOAD(true),
38 | BACKUP_AND_RESTORE(null),
39 | BACKUP_APP_SETTINGS(null),
40 | BACKUP_APP_DATABASE(null),
41 | BACKUP_APP_DATA(null),
42 | RESTORE_APP_DATA(null),
43 | RESET_APP_SETTINGS(null),
44 | LAST_BACKUP_TIME(""),
45 | NOTIFICATION_SETTINGS(null),
46 | REMINDER_MARK_ATTENDANCE(true),
47 | NOTIFY_MISSED_ATTENDANCE(true),
48 | ENABLE_NOTIFICATIONS(true),
49 | UPDATE_AVAILABLE_NOTIFICATION(true),
50 | NOTIFICATION_PERMISSION_DIALOG_SHOWN(false),
51 | SHOW_GITHUB_WARNING_DIALOG(true),
52 | FONT_FAMILY(CustomFontFamily.SYSTEM_FONT)
53 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_settings.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/settings/data/remote/repository/UpdateRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.settings.data.remote.repository
2 |
3 | import android.util.Log
4 | import `in`.hridayan.driftly.settings.data.remote.api.GitHubApi
5 | import `in`.hridayan.driftly.settings.data.remote.mapper.toDomain
6 | import `in`.hridayan.driftly.settings.domain.model.UpdateResult
7 | import `in`.hridayan.driftly.settings.domain.repository.UpdateRepository
8 | import io.ktor.client.network.sockets.SocketTimeoutException
9 | import kotlinx.io.IOException
10 |
11 | import java.net.ConnectException
12 | import java.net.UnknownHostException
13 | import java.nio.channels.UnresolvedAddressException
14 | import javax.inject.Inject
15 |
16 | class UpdateRepositoryImpl @Inject constructor(
17 | private val api: GitHubApi
18 | ) : UpdateRepository {
19 |
20 | override suspend fun fetchLatestRelease(includePrerelease:Boolean): UpdateResult {
21 | return try {
22 | Log.d("UpdateRepository", "Fetching latest release from GitHub")
23 | val response = api.fetchLatestRelease(includePrerelease).toDomain()
24 | UpdateResult.Success(response, response.tagName.isNotEmpty())
25 | } catch (e: SocketTimeoutException) {
26 | Log.e("UpdateRepository", "Timeout", e)
27 | UpdateResult.Timeout
28 | } catch (e: UnknownHostException) {
29 | Log.e("UpdateRepository", "No internet (UnknownHost)", e)
30 | UpdateResult.NetworkError
31 | } catch (e: ConnectException) {
32 | Log.e("UpdateRepository", "No internet (ConnectException)", e)
33 | UpdateResult.NetworkError
34 | } catch (e: UnresolvedAddressException) {
35 | Log.e("UpdateRepository", "No internet (UnresolvedAddressException)", e)
36 | UpdateResult.NetworkError
37 | } catch (e: IOException) {
38 | Log.e("UpdateRepository", "IO Exception", e)
39 | UpdateResult.NetworkError
40 | } catch (e: Exception) {
41 | Log.e("UpdateRepository", "Unknown error", e)
42 | UpdateResult.UnknownError
43 | }
44 | }
45 | }
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/calender/presentation/components/canvas/MonthYearPicker.kt:
--------------------------------------------------------------------------------
1 | @file:OptIn(ExperimentalMaterial3ExpressiveApi::class)
2 |
3 | package `in`.hridayan.driftly.calender.presentation.components.canvas
4 |
5 | import androidx.compose.foundation.clickable
6 | import androidx.compose.foundation.interaction.MutableInteractionSource
7 | import androidx.compose.foundation.layout.Row
8 | import androidx.compose.material.icons.Icons
9 | import androidx.compose.material.icons.rounded.ArrowDropDown
10 | import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
11 | import androidx.compose.material3.Icon
12 | import androidx.compose.material3.IconButton
13 | import androidx.compose.material3.IconButtonDefaults
14 | import androidx.compose.material3.MaterialTheme
15 | import androidx.compose.runtime.Composable
16 | import androidx.compose.runtime.remember
17 | import androidx.compose.ui.Alignment
18 | import androidx.compose.ui.Modifier
19 | import `in`.hridayan.driftly.core.common.LocalWeakHaptic
20 | import `in`.hridayan.driftly.core.presentation.components.text.AutoResizeableText
21 |
22 | @Composable
23 | fun MonthYearPicker(
24 | modifier: Modifier = Modifier, fullMonthName: String, year: Int, onClick: () -> Unit
25 | ) {
26 | val weakHaptic = LocalWeakHaptic.current
27 | Row(
28 | modifier = modifier.clickable(
29 | onClick = {
30 | weakHaptic()
31 | onClick()
32 | },
33 | interactionSource = remember { MutableInteractionSource() },
34 | indication = null,
35 | ), verticalAlignment = Alignment.CenterVertically
36 | ) {
37 | AutoResizeableText(
38 | text = "$fullMonthName $year", style = MaterialTheme.typography.titleSmall
39 | )
40 |
41 | IconButton(
42 | onClick = {
43 | weakHaptic()
44 | onClick()
45 | },
46 | shapes = IconButtonDefaults.shapes()
47 | ) {
48 | Icon(
49 | imageVector = Icons.Rounded.ArrowDropDown,
50 | contentDescription = "Dropdown icon"
51 | )
52 | }
53 | }
54 | }
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/crash_report.yml:
--------------------------------------------------------------------------------
1 | name: Crash Report
2 | description: Submit crash logs to help us fix issues
3 | title: '[Crash] '
4 | labels:
5 | - crash
6 | assignees: []
7 | body:
8 | - type: textarea
9 | id: crash-log
10 | attributes:
11 | label: Paste crash log
12 | description: Paste your crash log **inside triple backticks (```)** so it shows
13 | up properly formatted.
14 | placeholder: "```\njava.lang.RuntimeException: Example crash\n at com.example.MyClass.myMethod(MyClass.java:123)\n\
15 | \ \n```\n"
16 | validations:
17 | required: true
18 | - type: textarea
19 | id: additional-context
20 | attributes:
21 | label: Additional context
22 | description: You can write additional context here, e.g. what action exactly caused
23 | the crash
24 | placeholder: 'What caused the crash?
25 |
26 | In which screen did the app crash?
27 |
28 | '
29 | validations:
30 | required: false
31 | - type: markdown
32 | attributes:
33 | value: '**These fields are needed in case you used other crash reporting apps
34 | for logs:**'
35 | - type: input
36 | id: smartphone-device
37 | attributes:
38 | label: Device
39 | placeholder: e.g. Samsung Galaxy S23
40 | validations:
41 | required: true
42 | - type: input
43 | id: smartphone-os
44 | attributes:
45 | label: OS
46 | placeholder: e.g. Android 14
47 | validations:
48 | required: true
49 | - type: dropdown
50 | id: version
51 | attributes:
52 | label: Version
53 | description: What version of Driftly are you running?
54 | options:
55 | - v1.8.3 (Latest)
56 | - v1.8.2
57 | - v1.8.1
58 | - v1.8.0
59 | - v1.7.0
60 | - v1.6.0
61 | - v1.5.1
62 | - v1.5.0
63 | - v1.4.0
64 | - v1.3.0
65 | - v1.2.0
66 | - v1.1.0
67 | - v1.0.0
68 | default: 0
69 | validations:
70 | required: true
71 | - type: checkboxes
72 | id: checklist
73 | attributes:
74 | label: Checklist
75 | description: Carefully read and work through this checklist
76 | options:
77 | - label: I understand that the issue will be (ignored/closed) if I intentionally
78 | remove or skip any mandatory field.
79 | required: true
80 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/core/presentation/components/canvas/HorizontalProgressWave.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.core.presentation.components.canvas
2 |
3 | import androidx.compose.animation.core.LinearEasing
4 | import androidx.compose.animation.core.animateFloat
5 | import androidx.compose.animation.core.infiniteRepeatable
6 | import androidx.compose.animation.core.rememberInfiniteTransition
7 | import androidx.compose.animation.core.tween
8 | import androidx.compose.foundation.Canvas
9 | import androidx.compose.foundation.layout.fillMaxSize
10 | import androidx.compose.material3.MaterialTheme
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.runtime.getValue
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.graphics.Color
15 | import androidx.compose.ui.graphics.Path
16 | import kotlin.math.sin
17 |
18 | @Composable
19 | fun HorizontalProgressWave(
20 | modifier: Modifier = Modifier,
21 | progress: Float,
22 | waveSpeed: Int = 4000,
23 | waveFrequency: Float = 2f,
24 | waveColor: Color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.5f)
25 | ) {
26 | val animatedOffset by rememberInfiniteTransition(label = "wave")
27 | .animateFloat(
28 | initialValue = 0f,
29 | targetValue = 1f,
30 | animationSpec = infiniteRepeatable(animation = tween(waveSpeed, easing = LinearEasing)),
31 | label = "Wave Offset"
32 | )
33 |
34 | Canvas(modifier = modifier.fillMaxSize()) {
35 | val width = size.width
36 | val height = size.height
37 |
38 | val waveAmplitude = height * 0.05f
39 | val batteryLevelWidth = width * progress.coerceIn(0f, 1f)
40 |
41 | val path = Path().apply {
42 | moveTo(0f, 0f)
43 |
44 | for (i in 0..height.toInt()) {
45 | val y = i.toFloat()
46 | val x =
47 | batteryLevelWidth + sin((y / height + animatedOffset) * waveFrequency * Math.PI).toFloat() * waveAmplitude
48 | lineTo(x, y)
49 | }
50 |
51 | lineTo(0f, height)
52 | close()
53 | }
54 |
55 | drawPath(path = path, color = waveColor)
56 | }
57 | }
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/settings/presentation/provider/RadioGroupOptionsProvider.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.settings.presentation.provider
2 |
3 | import androidx.appcompat.app.AppCompatDelegate
4 | import `in`.hridayan.driftly.R
5 | import `in`.hridayan.driftly.core.domain.model.GithubReleaseType
6 | import `in`.hridayan.driftly.settings.domain.model.CustomFontFamily
7 | import `in`.hridayan.driftly.settings.domain.model.RadioButtonOptions
8 |
9 | class RadioGroupOptionsProvider {
10 | companion object {
11 | val darkModeOptions: List = listOf(
12 | RadioButtonOptions(
13 | value = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM,
14 | labelResId = R.string.system
15 | ),
16 | RadioButtonOptions(
17 | value = AppCompatDelegate.MODE_NIGHT_NO,
18 | labelResId = R.string.off
19 | ),
20 | RadioButtonOptions(
21 | value = AppCompatDelegate.MODE_NIGHT_YES,
22 | labelResId = R.string.on
23 | )
24 | )
25 |
26 | val updateChannelOptions: List = listOf(
27 | RadioButtonOptions(
28 | value = GithubReleaseType.STABLE,
29 | labelResId = R.string.stable
30 | ),
31 | RadioButtonOptions(
32 | value = GithubReleaseType.PRE_RELEASE,
33 | labelResId = R.string.pre_release
34 | ),
35 | )
36 |
37 | val fontStyleOptions: List = listOf(
38 | RadioButtonOptions(
39 | value = CustomFontFamily.SYSTEM_FONT,
40 | labelResId = R.string.system_font
41 | ),
42 | RadioButtonOptions(
43 | value = CustomFontFamily.ONE_UI_SANS,
44 | labelResId = R.string.one_ui_sans
45 | ),
46 | RadioButtonOptions(
47 | value = CustomFontFamily.MONOSPACE,
48 | labelResId = R.string.monospace
49 | ),
50 | RadioButtonOptions(
51 | value = CustomFontFamily.SANS_SERIF,
52 | labelResId = R.string.sans_serif
53 | )
54 | )
55 | }
56 | }
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/core/presentation/components/canvas/VerticalProgressWave.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.core.presentation.components.canvas
2 |
3 | import androidx.compose.animation.core.LinearEasing
4 | import androidx.compose.animation.core.animateFloat
5 | import androidx.compose.animation.core.infiniteRepeatable
6 | import androidx.compose.animation.core.rememberInfiniteTransition
7 | import androidx.compose.animation.core.tween
8 | import androidx.compose.foundation.Canvas
9 | import androidx.compose.foundation.layout.fillMaxSize
10 | import androidx.compose.foundation.layout.fillMaxWidth
11 | import androidx.compose.material3.MaterialTheme
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.runtime.getValue
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.graphics.Color
16 | import androidx.compose.ui.graphics.Path
17 | import kotlin.math.sin
18 |
19 | @Composable
20 | fun VerticalProgressWave(
21 | modifier: Modifier = Modifier, progress: Float,
22 | waveSpeed: Int = 4000,
23 | waveFrequency: Float = 2f,
24 | waveColor: Color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.5f)
25 | ) {
26 | val animatedOffset by rememberInfiniteTransition(label = "wave")
27 | .animateFloat(
28 | initialValue = 0f,
29 | targetValue = 1f,
30 | animationSpec = infiniteRepeatable(animation = tween(waveSpeed, easing = LinearEasing)),
31 | label = "Wave Offset"
32 | )
33 |
34 | Canvas(modifier = modifier.fillMaxWidth()) {
35 | val width = size.width
36 | val height = size.height
37 |
38 | if (height == 0f || width == 0f) return@Canvas
39 |
40 | val waveHeight = height * 0.1f
41 | val batteryLevelHeight = height * (1f - progress)
42 |
43 | val path = Path().apply {
44 | moveTo(0f, batteryLevelHeight)
45 | for (i in 0..width.toInt() step 1) {
46 | val x = i.toFloat()
47 | val y =
48 | batteryLevelHeight + sin((x / width + animatedOffset) * waveFrequency * Math.PI).toFloat() * waveHeight
49 | lineTo(x, y)
50 | }
51 | lineTo(width, height)
52 | lineTo(0f, height)
53 | close()
54 | }
55 |
56 | drawPath(path = path, color = waveColor)
57 | }
58 | }
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/core/utils/MiUiCheck.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.core.utils
2 |
3 | import android.os.Build
4 | import android.util.Log
5 | import java.io.BufferedReader
6 | import java.io.IOException
7 | import java.io.InputStreamReader
8 | import java.util.Locale
9 |
10 | object MiUiCheck {
11 | /**
12 | * Check if the device is running on MIUI.
13 | * By default, HyperOS is excluded from the check.
14 | * If you want to include HyperOS in the check, set excludeHyperOS to false.
15 | *
16 | * @param excludeHyperOS Whether to exclude HyperOS
17 | * @return True if the device is running on MIUI, false otherwise
18 | */
19 | fun isMiui(excludeHyperOS: Boolean): Boolean {
20 | // Check if the device is manufactured by Xiaomi, Redmi, or POCO.
21 | val brand = Build.BRAND.lowercase(Locale.getDefault())
22 | val validBrands: MutableSet =
23 | HashSet(mutableListOf("xiaomi", "redmi", "poco"))
24 | if (!validBrands.contains(brand)) return false
25 |
26 | // This property is present in both MIUI and HyperOS.
27 | val miuiVersion = getProperty("ro.miui.ui.version.name")
28 | val isMiui = miuiVersion != null && !miuiVersion.isBlank()
29 | // This property is exclusive to HyperOS only and isn't present in MIUI.
30 | val hyperOSVersion = getProperty("ro.mi.os.version.name")
31 | val isHyperOS = hyperOSVersion != null && !hyperOSVersion.isBlank()
32 | return isMiui && (!excludeHyperOS || !isHyperOS)
33 | }
34 |
35 | val isMiui: Boolean
36 | /**
37 | * Overloaded method to maintain the default behavior of excluding HyperOS.
38 | *
39 | * @return True if the device is running on MIUI, false otherwise
40 | */
41 | get() = isMiui(true)
42 |
43 | // Private function to get the property value from build.prop.
44 | private fun getProperty(property: String?): String? {
45 | try {
46 | val process = Runtime.getRuntime().exec("getprop $property")
47 | BufferedReader(InputStreamReader(process.inputStream), 1024).use { input ->
48 | return input.readLine()
49 | }
50 | } catch (e: IOException) {
51 | Log.e("MiuiCheck", "Unable to read property $property", e)
52 | return null
53 | }
54 | }
55 | }
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/settings/presentation/page/autoupdate/viewmodel/AutoUpdateViewModel.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.settings.presentation.page.autoupdate.viewmodel
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import dagger.hilt.android.lifecycle.HiltViewModel
6 | import `in`.hridayan.driftly.BuildConfig
7 | import `in`.hridayan.driftly.core.domain.model.DownloadState
8 | import `in`.hridayan.driftly.core.domain.usecase.DownloadApkUseCase
9 | import `in`.hridayan.driftly.settings.data.local.SettingsKeys
10 | import `in`.hridayan.driftly.settings.domain.model.UpdateResult
11 | import `in`.hridayan.driftly.settings.domain.repository.SettingsRepository
12 | import `in`.hridayan.driftly.settings.domain.usecase.CheckUpdateUseCase
13 | import kotlinx.coroutines.Dispatchers
14 | import kotlinx.coroutines.flow.MutableSharedFlow
15 | import kotlinx.coroutines.flow.MutableStateFlow
16 | import kotlinx.coroutines.flow.asSharedFlow
17 | import kotlinx.coroutines.flow.asStateFlow
18 | import kotlinx.coroutines.launch
19 | import javax.inject.Inject
20 |
21 | @HiltViewModel
22 | class AutoUpdateViewModel @Inject constructor(
23 | private val settingsRepository: SettingsRepository,
24 | private val checkUpdateUseCase: CheckUpdateUseCase,
25 | private val downloadApkUseCase: DownloadApkUseCase
26 | ) : ViewModel() {
27 | private val _updateEvents = MutableSharedFlow()
28 | val updateEvents = _updateEvents.asSharedFlow()
29 |
30 | fun checkForUpdates(includePrerelease: Boolean) {
31 | viewModelScope.launch {
32 | val result = checkUpdateUseCase(BuildConfig.VERSION_NAME, includePrerelease)
33 | _updateEvents.emit(result)
34 | }
35 | }
36 |
37 | fun select(option: Int) {
38 | viewModelScope.launch {
39 | settingsRepository.setInt(SettingsKeys.GITHUB_RELEASE_TYPE, option)
40 | }
41 | }
42 |
43 | private val _downloadState = MutableStateFlow(DownloadState.Idle)
44 | val downloadState = _downloadState.asStateFlow()
45 |
46 | fun downloadApk(url: String, fileName: String) {
47 | viewModelScope.launch(Dispatchers.IO) {
48 | downloadApkUseCase(url, fileName) {
49 | _downloadState.value = it
50 | }
51 | }
52 | }
53 |
54 | fun cancelDownload() {
55 | downloadApkUseCase.cancel()
56 | }
57 | }
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yml:
--------------------------------------------------------------------------------
1 | name: Bug Report
2 | description: Create a report to help us improve
3 | title: '[Bug] '
4 | labels:
5 | - bug
6 | assignees: []
7 | body:
8 | - type: textarea
9 | id: describe-bug
10 | attributes:
11 | label: Describe the bug
12 | description: A clear and concise description of what the bug is.
13 | placeholder: What's the issue you're experiencing?
14 | validations:
15 | required: true
16 | - type: textarea
17 | id: steps-to-reproduce
18 | attributes:
19 | label: To Reproduce
20 | description: Steps to reproduce the behavior
21 | placeholder: '1. Go to ''...''
22 |
23 | 2. Click on ''....''
24 |
25 | 3. Scroll down to ''....''
26 |
27 | 4. See error
28 |
29 | '
30 | - type: textarea
31 | id: expected-behavior
32 | attributes:
33 | label: Expected behavior
34 | description: A clear and concise description of what you expected to happen.
35 | placeholder: What did you expect to happen instead?
36 | - type: textarea
37 | id: screenshots
38 | attributes:
39 | label: Screenshots
40 | description: If applicable, add screenshots to help explain your problem.
41 | placeholder: You can drag and drop images into this issue.
42 | - type: markdown
43 | attributes:
44 | value: '**Smartphone (please complete the following information):**'
45 | - type: input
46 | id: smartphone-device
47 | attributes:
48 | label: Device
49 | placeholder: e.g. Samsung Galaxy S23
50 | validations:
51 | required: true
52 | - type: input
53 | id: smartphone-os
54 | attributes:
55 | label: OS
56 | placeholder: e.g. Android 14
57 | validations:
58 | required: true
59 | - type: dropdown
60 | id: version
61 | attributes:
62 | label: Version
63 | description: What version of Driftly are you running?
64 | options:
65 | - v1.8.3 (Latest)
66 | - v1.8.2
67 | - v1.8.1
68 | - v1.8.0
69 | - v1.7.0
70 | - v1.6.0
71 | - v1.5.1
72 | - v1.5.0
73 | - v1.4.0
74 | - v1.3.0
75 | - v1.2.0
76 | - v1.1.0
77 | - v1.0.0
78 | default: 0
79 | validations:
80 | required: true
81 | - type: checkboxes
82 | id: checklist
83 | attributes:
84 | label: Checklist
85 | description: 'Carefully read and work through this check list
86 |
87 | '
88 | options:
89 | - label: I understand that the issue will be (ignored/closed) if I intentionally
90 | remove or skip any mandatory field.
91 | required: true
92 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/core/presentation/components/text/AutoResizeableText.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.core.presentation.components.text
2 |
3 |
4 | import androidx.compose.foundation.text.TextAutoSize
5 | import androidx.compose.material3.LocalTextStyle
6 | import androidx.compose.material3.Text
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.Modifier
9 | import androidx.compose.ui.graphics.Color
10 | import androidx.compose.ui.text.TextLayoutResult
11 | import androidx.compose.ui.text.TextStyle
12 | import androidx.compose.ui.text.font.FontFamily
13 | import androidx.compose.ui.text.font.FontStyle
14 | import androidx.compose.ui.text.font.FontWeight
15 | import androidx.compose.ui.text.style.TextAlign
16 | import androidx.compose.ui.text.style.TextDecoration
17 | import androidx.compose.ui.text.style.TextOverflow
18 | import androidx.compose.ui.unit.TextUnit
19 | import androidx.compose.ui.unit.sp
20 |
21 | @Composable
22 | fun AutoResizeableText(
23 | text: String,
24 | modifier: Modifier = Modifier,
25 | color: Color = Color.Unspecified,
26 | fontSize: TextUnit = TextUnit.Unspecified,
27 | fontStyle: FontStyle? = null,
28 | fontWeight: FontWeight? = null,
29 | fontFamily: FontFamily? = null,
30 | letterSpacing: TextUnit = TextUnit.Unspecified,
31 | textDecoration: TextDecoration? = null,
32 | textAlign: TextAlign? = null,
33 | lineHeight: TextUnit = TextUnit.Unspecified,
34 | overflow: TextOverflow = TextOverflow.Clip,
35 | softWrap: Boolean = true,
36 | maxLines: Int = 1,
37 | minLines: Int = 1,
38 | onTextLayout: ((TextLayoutResult) -> Unit)? = null,
39 | style: TextStyle = LocalTextStyle.current,
40 | minFontSize: TextUnit = 6.sp
41 | ) {
42 | Text(
43 | text = text,
44 | modifier = modifier,
45 | color = color,
46 | fontSize = fontSize,
47 | fontStyle = fontStyle,
48 | fontWeight = fontWeight,
49 | fontFamily = fontFamily,
50 | letterSpacing = letterSpacing,
51 | textDecoration = textDecoration,
52 | textAlign = textAlign,
53 | lineHeight = lineHeight,
54 | overflow = overflow,
55 | softWrap = softWrap,
56 | maxLines = maxLines,
57 | minLines = minLines,
58 | onTextLayout = onTextLayout,
59 | style = style,
60 | autoSize = TextAutoSize.StepBased(
61 | maxFontSize = style.fontSize,
62 | minFontSize = minFontSize,
63 | )
64 | )
65 | }
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/notification/NotificationSetup.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.notification
2 |
3 | import android.Manifest
4 | import android.content.Context
5 | import androidx.annotation.RequiresPermission
6 | import `in`.hridayan.driftly.R
7 | import `in`.hridayan.driftly.notification.helper.NotificationHelper
8 |
9 | object NotificationSetup {
10 | const val ATTENDANCE_CHANNEL_ID = "attendance_channel"
11 | const val UPDATE_CHANNEL_ID = "update_channel"
12 |
13 | @RequiresPermission(Manifest.permission.POST_NOTIFICATIONS)
14 | fun showAttendanceReminderNotification(context: Context) {
15 | NotificationHelper.showNotification(
16 | context = context,
17 | channelId = ATTENDANCE_CHANNEL_ID,
18 | channelName = context.getString(R.string.attendance),
19 | channelDescription = context.getString(R.string.daily_reminder_to_mark_attendance),
20 | notificationId = 1001,
21 | title = context.getString(R.string.mark_your_attendance),
22 | message = context.getString(R.string.des_mark_your_attendance),
23 | smallIconResId = R.drawable.ic_check_circle,
24 | )
25 | }
26 |
27 | @RequiresPermission(Manifest.permission.POST_NOTIFICATIONS)
28 | fun showMissedAttendanceNotification(context: Context) {
29 | NotificationHelper.showNotification(
30 | context = context,
31 | channelId = ATTENDANCE_CHANNEL_ID,
32 | channelName = context.getString(R.string.attendance),
33 | channelDescription = context.getString(R.string.daily_reminder_to_mark_attendance),
34 | notificationId = 1002,
35 | title = context.getString(R.string.mark_your_attendance),
36 | message = context.getString(R.string.des_attendance_missed),
37 | smallIconResId = R.drawable.ic_check_circle,
38 | )
39 | }
40 |
41 | @RequiresPermission(Manifest.permission.POST_NOTIFICATIONS)
42 | fun showUpdateAvailableNotification(context: Context) {
43 | NotificationHelper.showNotification(
44 | context = context,
45 | channelId = UPDATE_CHANNEL_ID,
46 | channelName = context.getString(R.string.updates),
47 | channelDescription = context.getString(R.string.des_notify_update_available),
48 | notificationId = 1003,
49 | title = context.getString(R.string.update_available),
50 | message = context.getString(R.string.des_update_available),
51 | smallIconResId = R.drawable.ic_release_alert,
52 | )
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/core/data/repository/AttendanceRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.core.data.repository
2 |
3 | import `in`.hridayan.driftly.core.data.database.AttendanceDao
4 | import `in`.hridayan.driftly.core.data.model.AttendanceEntity
5 | import `in`.hridayan.driftly.core.domain.model.AttendanceStatus
6 | import `in`.hridayan.driftly.core.domain.repository.AttendanceRepository
7 | import kotlinx.coroutines.flow.Flow
8 | import javax.inject.Inject
9 | import javax.inject.Singleton
10 |
11 | @Singleton
12 | class AttendanceRepositoryImpl @Inject constructor(
13 | private val dao: AttendanceDao
14 | ) : AttendanceRepository {
15 |
16 | override suspend fun insertAttendance(attendance: AttendanceEntity) {
17 | dao.insertAttendance(attendance)
18 | }
19 |
20 | override suspend fun insertAllAttendances(attendances: List) {
21 | dao.insertAllAttendances(attendances)
22 | }
23 |
24 | override suspend fun updateAttendance(attendance: AttendanceEntity) {
25 | dao.updateAttendance(attendance)
26 | }
27 |
28 | override suspend fun deleteAttendance(subjectId: Int, date: String) {
29 | dao.deleteBySubjectAndDate(subjectId, date)
30 | }
31 |
32 | override suspend fun deleteAllAttendances() {
33 | dao.deleteAllAttendances()
34 | }
35 |
36 | override suspend fun deleteAllAttendanceForSubject(subjectId: Int) {
37 | dao.deleteAllAttendanceForSubject(subjectId)
38 | }
39 |
40 | override suspend fun getAllAttendancesOnce(): List =
41 | dao.getAllAttendancesOnce()
42 |
43 | override suspend fun hasUnmarkedAttendanceForDate(date: String): Boolean =
44 | dao.hasUnmarkedAttendanceForDate(date)
45 |
46 | override fun getAttendanceForSubject(subjectId: Int): Flow> =
47 | dao.getAttendanceForSubjectFlow(subjectId)
48 |
49 | override fun getTotalCountByStatus(status: AttendanceStatus): Flow =
50 | dao.getTotalCountByStatus(status)
51 |
52 | override fun getCountBySubjectAndStatus(subjectId: Int, status: AttendanceStatus): Flow =
53 | dao.getCountBySubjectAndStatus(subjectId, status)
54 |
55 | override fun getPresentCountForMonth(subjectId: Int, year: Int, month: Int): Flow =
56 | dao.getPresentCountForMonth(subjectId, year, month)
57 |
58 | override fun getAbsentCountForMonth(subjectId: Int, year: Int, month: Int): Flow =
59 | dao.getAbsentCountForMonth(subjectId, year, month)
60 |
61 | override fun getTotalCountForMonth(subjectId: Int, year: Int, month: Int): Flow =
62 | dao.getTotalCountForMonth(subjectId, year, month)
63 | }
64 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.setContent
6 | import androidx.activity.enableEdgeToEdge
7 | import androidx.activity.viewModels
8 | import androidx.compose.foundation.layout.fillMaxSize
9 | import androidx.compose.material3.MaterialTheme
10 | import androidx.compose.material3.Surface
11 | import androidx.compose.ui.Modifier
12 | import androidx.lifecycle.lifecycleScope
13 | import dagger.hilt.android.AndroidEntryPoint
14 | import `in`.hridayan.driftly.core.common.CompositionLocals
15 | import `in`.hridayan.driftly.core.common.LocalSeedColor
16 | import `in`.hridayan.driftly.core.domain.provider.SeedColorProvider
17 | import `in`.hridayan.driftly.core.domain.model.GithubReleaseType
18 | import `in`.hridayan.driftly.core.presentation.AppEntry
19 | import `in`.hridayan.driftly.core.presentation.theme.DriftlyTheme
20 | import `in`.hridayan.driftly.settings.data.local.SettingsKeys
21 | import `in`.hridayan.driftly.settings.presentation.page.autoupdate.viewmodel.AutoUpdateViewModel
22 | import `in`.hridayan.driftly.settings.presentation.viewmodel.SettingsViewModel
23 | import kotlinx.coroutines.flow.first
24 | import kotlinx.coroutines.launch
25 |
26 | @AndroidEntryPoint
27 | class MainActivity : ComponentActivity() {
28 | private val settingsViewModel: SettingsViewModel by viewModels()
29 | private val autoUpdateViewModel: AutoUpdateViewModel by viewModels()
30 |
31 | override fun onCreate(savedInstanceState: Bundle?) {
32 | super.onCreate(savedInstanceState)
33 |
34 | lifecycleScope.launch {
35 | val autoUpdateEnabled = settingsViewModel.getBoolean(SettingsKeys.AUTO_UPDATE).first()
36 | val includePrerelease =
37 | settingsViewModel.getInt(SettingsKeys.GITHUB_RELEASE_TYPE)
38 | .first() == GithubReleaseType.PRE_RELEASE
39 |
40 | if (autoUpdateEnabled) {
41 | autoUpdateViewModel.checkForUpdates(
42 | includePrerelease = includePrerelease
43 | )
44 | }
45 | }
46 |
47 | enableEdgeToEdge()
48 | setContent {
49 | CompositionLocals {
50 | SeedColorProvider.setSeedColor(LocalSeedColor.current)
51 |
52 | DriftlyTheme {
53 | Surface(
54 | modifier = Modifier.Companion.fillMaxSize(),
55 | color = MaterialTheme.colorScheme.surface
56 | ) {
57 | AppEntry()
58 | }
59 | }
60 | }
61 | }
62 | }
63 | }
64 |
65 |
66 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/settings/presentation/components/item/ChangelogItemLayout.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.settings.presentation.components.item
2 |
3 | import androidx.compose.foundation.border
4 | import androidx.compose.foundation.layout.Arrangement
5 | import androidx.compose.foundation.layout.Box
6 | import androidx.compose.foundation.layout.Column
7 | import androidx.compose.foundation.layout.Row
8 | import androidx.compose.foundation.layout.fillMaxWidth
9 | import androidx.compose.foundation.layout.size
10 | import androidx.compose.foundation.shape.CircleShape
11 | import androidx.compose.material3.MaterialTheme
12 | import androidx.compose.material3.Text
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.layout.FirstBaseline
16 | import androidx.compose.ui.res.stringResource
17 | import androidx.compose.ui.text.font.FontWeight
18 | import androidx.compose.ui.unit.dp
19 | import `in`.hridayan.driftly.BuildConfig
20 | import `in`.hridayan.driftly.R
21 | import `in`.hridayan.driftly.core.presentation.components.text.AutoResizeableText
22 |
23 | @Composable
24 | fun ChangelogItemLayout(
25 | modifier: Modifier = Modifier,
26 | versionName: String,
27 | changelog: List,
28 | ) {
29 | val isLatestVersion = versionName == BuildConfig.VERSION_NAME
30 | Column(
31 | modifier = modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(25.dp)
32 | ) {
33 | AutoResizeableText(
34 | modifier = Modifier.fillMaxWidth(),
35 | text = stringResource(R.string.version) + "\t\t$versionName",
36 | style = if (isLatestVersion) MaterialTheme.typography.headlineMedium else MaterialTheme.typography.headlineSmall,
37 | color = if (isLatestVersion) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface,
38 | fontWeight = FontWeight.Bold
39 | )
40 |
41 | changelog.forEach { item ->
42 | Row(
43 | modifier = Modifier.fillMaxWidth(),
44 | horizontalArrangement = Arrangement.spacedBy(10.dp)
45 | ) {
46 | Box(
47 | modifier = Modifier
48 | .size(10.dp)
49 | .alignBy { it.measuredHeight }
50 | .border(2.dp, MaterialTheme.colorScheme.primary, CircleShape)
51 | )
52 |
53 | Text(
54 | text = item,
55 | style = MaterialTheme.typography.bodySmall,
56 | color = MaterialTheme.colorScheme.onSurface,
57 | modifier = Modifier.alignBy(FirstBaseline)
58 | )
59 | }
60 | }
61 | }
62 | }
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/calender/presentation/components/modifiers/Modifiers.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.calender.presentation.components.modifiers
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.border
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.foundation.shape.CircleShape
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.draw.clip
9 | import androidx.compose.ui.draw.drawBehind
10 | import androidx.compose.ui.geometry.Offset
11 | import androidx.compose.ui.geometry.Size
12 | import androidx.compose.ui.graphics.Color
13 | import androidx.compose.ui.unit.dp
14 | import `in`.hridayan.driftly.core.domain.model.StreakType
15 |
16 | fun Modifier.streakModifier(
17 | showStreak: Boolean = true,
18 | streakType: StreakType,
19 | circleBg: Color,
20 | circleFg: Color,
21 | streakBandColor: Color,
22 | isToday: Boolean,
23 | ): Modifier {
24 | val baseCircleModifier = this
25 | .padding(4.dp)
26 | .clip(CircleShape)
27 | .background(circleBg)
28 | .then(
29 | if (isToday) Modifier.border(1.dp, circleFg, CircleShape) else Modifier
30 | )
31 |
32 | if (!showStreak) return baseCircleModifier
33 |
34 | else return when (streakType) {
35 | StreakType.START -> this
36 | .drawBehind {
37 | val paddingY = 8.dp.toPx()
38 | val startX = size.width * 0.5f
39 | drawRect(
40 | color = streakBandColor,
41 | topLeft = Offset(startX, paddingY),
42 | size = Size(size.width - startX, size.height - 2 * paddingY)
43 | )
44 | }
45 | .then(baseCircleModifier)
46 |
47 | StreakType.END -> this
48 | .drawBehind {
49 | val paddingY = 8.dp.toPx()
50 | val endX = size.width * 0.5f
51 | drawRect(
52 | color = streakBandColor,
53 | topLeft = Offset(0f, paddingY),
54 | size = Size(endX, size.height - 2 * paddingY)
55 | )
56 | }
57 | .then(baseCircleModifier)
58 |
59 | StreakType.MIDDLE -> this
60 | .padding(vertical = 8.dp)
61 | .background(streakBandColor)
62 | .then(
63 | if (isToday) Modifier
64 | .padding(horizontal = 8.dp)
65 | .border(1.dp, circleBg, CircleShape)
66 | else Modifier
67 | )
68 |
69 | StreakType.NONE -> baseCircleModifier
70 | }
71 | }
--------------------------------------------------------------------------------
/docs/translations-dark.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/settings/presentation/page/lookandfeel/viewmodel/LookAndFeelViewModel.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.settings.presentation.page.lookandfeel.viewmodel
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import dagger.hilt.android.lifecycle.HiltViewModel
6 | import `in`.hridayan.driftly.core.presentation.provider.SeedColor
7 | import `in`.hridayan.driftly.settings.data.local.SettingsKeys
8 | import `in`.hridayan.driftly.settings.domain.repository.SettingsRepository
9 | import kotlinx.coroutines.Dispatchers
10 | import kotlinx.coroutines.flow.MutableStateFlow
11 | import kotlinx.coroutines.flow.StateFlow
12 | import kotlinx.coroutines.launch
13 | import javax.inject.Inject
14 |
15 | @HiltViewModel
16 | class LookAndFeelViewModel @Inject constructor(
17 | private val settingsRepository: SettingsRepository,
18 | ) : ViewModel() {
19 | private var lastSeed: SeedColor? = null
20 |
21 | private val _isCheckedMatchCase = MutableStateFlow(false)
22 | val isCheckedMatchCase: StateFlow = _isCheckedMatchCase
23 |
24 | private val _isCheckedBold = MutableStateFlow(false)
25 | val isCheckedBold: StateFlow = _isCheckedBold
26 |
27 | private val _isCheckedItalic = MutableStateFlow(false)
28 | val isCheckedItalic: StateFlow = _isCheckedItalic
29 |
30 | private val _isCheckedUnderline = MutableStateFlow(false)
31 | val isCheckedUnderline: StateFlow = _isCheckedUnderline
32 |
33 | fun setSeedColor(seed: SeedColor) {
34 | if (seed == lastSeed) return
35 | lastSeed = seed
36 |
37 | viewModelScope.launch(Dispatchers.IO) {
38 | settingsRepository.setInt(SettingsKeys.PRIMARY_SEED, seed.primary)
39 | settingsRepository.setInt(SettingsKeys.SECONDARY_SEED, seed.secondary)
40 | settingsRepository.setInt(SettingsKeys.TERTIARY_SEED, seed.tertiary)
41 | }
42 | }
43 |
44 | fun disableDynamicColors() {
45 | viewModelScope.launch(Dispatchers.IO) {
46 | settingsRepository.setBoolean(SettingsKeys.DYNAMIC_COLORS, false)
47 | }
48 | }
49 |
50 | fun toggleMatchCase() {
51 | _isCheckedMatchCase.value = !_isCheckedMatchCase.value
52 | }
53 |
54 | fun toggleBold() {
55 | _isCheckedBold.value = !_isCheckedBold.value
56 | }
57 |
58 | fun toggleItalic() {
59 | _isCheckedItalic.value = !_isCheckedItalic.value
60 | }
61 |
62 | fun toggleUnderline() {
63 | _isCheckedUnderline.value = !_isCheckedUnderline.value
64 | }
65 |
66 | fun formatClear() {
67 | _isCheckedMatchCase.value = false
68 | _isCheckedBold.value = false
69 | _isCheckedItalic.value = false
70 | _isCheckedUnderline.value = false
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/docs/translations-light.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/notification/helper/NotificationHelper.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.notification.helper
2 |
3 | import android.Manifest
4 | import android.app.NotificationChannel
5 | import android.app.NotificationManager
6 | import android.app.PendingIntent
7 | import android.content.Context
8 | import android.content.Intent
9 | import androidx.annotation.DrawableRes
10 | import androidx.annotation.RequiresPermission
11 | import androidx.core.app.NotificationCompat
12 | import androidx.core.app.NotificationManagerCompat
13 | import `in`.hridayan.driftly.MainActivity
14 | import `in`.hridayan.driftly.R
15 |
16 | object NotificationHelper {
17 |
18 | @RequiresPermission(Manifest.permission.POST_NOTIFICATIONS)
19 | fun showNotification(
20 | context: Context,
21 | channelId: String,
22 | channelName: String,
23 | channelDescription: String,
24 | notificationId: Int,
25 | title: String,
26 | message: String,
27 | @DrawableRes smallIconResId: Int,
28 | priority: Int = NotificationCompat.PRIORITY_DEFAULT
29 | ) {
30 | createNotificationChannel(context, channelId, channelName, channelDescription)
31 |
32 | val notificationSmallIcon =
33 | if (smallIconResId == 0) R.drawable.ic_check_circle else smallIconResId
34 |
35 | val intent = Intent(context, MainActivity::class.java).apply {
36 | flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
37 | }
38 |
39 | val pendingIntent = PendingIntent.getActivity(
40 | context,
41 | notificationId,
42 | intent,
43 | PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
44 | )
45 |
46 | val notification = NotificationCompat.Builder(context, channelId)
47 | .setSmallIcon(notificationSmallIcon)
48 | .setContentTitle(title)
49 | .setContentText(message)
50 | .setPriority(priority)
51 | .setAutoCancel(true)
52 | .setContentIntent(pendingIntent)
53 | .build()
54 |
55 | NotificationManagerCompat.from(context).notify(notificationId, notification)
56 | }
57 |
58 | private fun createNotificationChannel(
59 | context: Context,
60 | channelId: String,
61 | name: String,
62 | descriptionText: String
63 | ) {
64 | val importance = NotificationManager.IMPORTANCE_HIGH
65 | val channel = NotificationChannel(channelId, name, importance).apply {
66 | description = descriptionText
67 | }
68 | val notificationManager =
69 | context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
70 | notificationManager.createNotificationChannel(channel)
71 | }
72 | }
--------------------------------------------------------------------------------
/app/src/main/java/in/hridayan/driftly/home/presentation/components/label/Label.kt:
--------------------------------------------------------------------------------
1 | package `in`.hridayan.driftly.home.presentation.components.label
2 |
3 | import androidx.compose.animation.core.Animatable
4 | import androidx.compose.animation.core.FastOutSlowInEasing
5 | import androidx.compose.animation.core.tween
6 | import androidx.compose.foundation.background
7 | import androidx.compose.foundation.border
8 | import androidx.compose.foundation.clickable
9 | import androidx.compose.foundation.layout.Box
10 | import androidx.compose.foundation.layout.padding
11 | import androidx.compose.foundation.layout.wrapContentSize
12 | import androidx.compose.material3.MaterialTheme
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.runtime.LaunchedEffect
15 | import androidx.compose.runtime.remember
16 | import androidx.compose.ui.Alignment
17 | import androidx.compose.ui.Modifier
18 | import androidx.compose.ui.draw.clip
19 | import androidx.compose.ui.draw.scale
20 | import androidx.compose.ui.graphics.Color
21 | import androidx.compose.ui.unit.dp
22 | import `in`.hridayan.driftly.core.presentation.theme.Shape
23 | import `in`.hridayan.driftly.core.common.LocalWeakHaptic
24 | import `in`.hridayan.driftly.core.presentation.components.text.AutoResizeableText
25 | import kotlinx.coroutines.delay
26 | import kotlin.random.Random
27 |
28 | @Composable
29 | fun Label(
30 | modifier: Modifier = Modifier,
31 | text: String,
32 | labelColor: Color,
33 | strokeColor: Color,
34 | onClick: () -> Unit = {}
35 | ) {
36 | val weakHaptic = LocalWeakHaptic.current
37 | val animatedScale = remember { Animatable(0f) }
38 | val randomDelay = remember { Random.nextInt(250, 750) }
39 |
40 | LaunchedEffect(text) {
41 | delay(randomDelay.toLong())
42 | animatedScale.animateTo(
43 | targetValue = 1f.coerceIn(0.3f, 1f),
44 | animationSpec = tween(
45 | durationMillis = 600,
46 | easing = FastOutSlowInEasing
47 | )
48 | )
49 | }
50 |
51 | Box(
52 | modifier = modifier
53 | .wrapContentSize()
54 | .scale(animatedScale.value)
55 | .clip(Shape.cardCornerLarge)
56 | .background(labelColor)
57 | .border(
58 | width = Shape.labelStroke,
59 | shape = Shape.cardCornerLarge,
60 | color = strokeColor,
61 | )
62 | .clickable(enabled = true, onClick = {
63 | onClick()
64 | weakHaptic()
65 | })
66 | .padding(horizontal = 10.dp, vertical = 5.dp),
67 | contentAlignment = Alignment.Center
68 | ) {
69 | AutoResizeableText(
70 | text = text,
71 | color = strokeColor,
72 | style = MaterialTheme.typography.labelMedium,
73 | )
74 | }
75 | }
--------------------------------------------------------------------------------