├── app
├── .gitignore
├── keystore.jks
├── src
│ ├── main
│ │ ├── ic_launcher-playstore.png
│ │ ├── res
│ │ │ ├── ic_launcher-playstore.png
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── ic_launcher_round.png
│ │ │ │ └── ic_launcher_foreground.png
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── ic_launcher_round.png
│ │ │ │ └── ic_launcher_foreground.png
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── ic_launcher_round.png
│ │ │ │ └── ic_launcher_foreground.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── ic_launcher_round.png
│ │ │ │ └── ic_launcher_foreground.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── ic_launcher_round.png
│ │ │ │ └── ic_launcher_foreground.png
│ │ │ ├── values
│ │ │ │ ├── ic_launcher_background.xml
│ │ │ │ ├── theme.xml
│ │ │ │ └── strings.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher_round.xml
│ │ │ │ └── ic_launcher.xml
│ │ │ ├── drawable
│ │ │ │ ├── baseline_add_24.xml
│ │ │ │ ├── baseline_warning_24.xml
│ │ │ │ ├── baseline_delete_24.xml
│ │ │ │ ├── baseline_arrow_back_24.xml
│ │ │ │ ├── baseline_keyboard_arrow_left_24.xml
│ │ │ │ ├── baseline_keyboard_arrow_right_24.xml
│ │ │ │ ├── baseline_privacy_tip_24.xml
│ │ │ │ ├── baseline_person_24.xml
│ │ │ │ ├── baseline_contrast_24.xml
│ │ │ │ ├── baseline_edit_24.xml
│ │ │ │ ├── baseline_calendar_today_24.xml
│ │ │ │ ├── baseline_table_chart_24.xml
│ │ │ │ ├── baseline_save_24.xml
│ │ │ │ ├── baseline_colorize_24.xml
│ │ │ │ ├── baseline_access_time_24.xml
│ │ │ │ ├── baseline_format_color_fill_24.xml
│ │ │ │ ├── baseline_color_lens_24.xml
│ │ │ │ ├── baseline_palette_24.xml
│ │ │ │ ├── github.xml
│ │ │ │ └── baseline_settings_24.xml
│ │ │ ├── xml
│ │ │ │ └── data_extraction_rules.xml
│ │ │ └── values-ru
│ │ │ │ └── strings.xml
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── github
│ │ │ │ └── rahul_gill
│ │ │ │ └── attendance
│ │ │ │ ├── util
│ │ │ │ ├── Constants.kt
│ │ │ │ ├── ApplicationContextInitializer.kt
│ │ │ │ ├── DateTimeFormats.kt
│ │ │ │ └── Preference.kt
│ │ │ │ ├── db
│ │ │ │ ├── CourseDetailsOverallItem.kt
│ │ │ │ ├── ExtraClassDetails.kt
│ │ │ │ ├── ClassDetail.kt
│ │ │ │ ├── ExtraClassTimings.kt
│ │ │ │ ├── CourseClassStatus.kt
│ │ │ │ ├── SqldelightAdapters.kt
│ │ │ │ ├── FutureThingCalculations.kt
│ │ │ │ └── TodayCourseItem.kt
│ │ │ │ ├── AttendanceApp.kt
│ │ │ │ ├── MainActivity.kt
│ │ │ │ ├── prefs
│ │ │ │ └── PreferenceManager.kt
│ │ │ │ └── ui
│ │ │ │ ├── comps
│ │ │ │ ├── Theme.kt
│ │ │ │ ├── ScheduleItem.kt
│ │ │ │ ├── SetClassStatusSheet.kt
│ │ │ │ ├── Dialog.kt
│ │ │ │ ├── Tabs.kt
│ │ │ │ ├── Preference.kt
│ │ │ │ ├── AddClassBottomSheet.kt
│ │ │ │ └── Popup.kt
│ │ │ │ └── screens
│ │ │ │ ├── CourseEditScreen.kt
│ │ │ │ └── CreateCourseScreen.kt
│ │ ├── sqldelight
│ │ │ ├── migrations
│ │ │ │ └── 1.sqm
│ │ │ └── com
│ │ │ │ └── github
│ │ │ │ └── rahul_gill
│ │ │ │ └── attendance
│ │ │ │ └── app.sq
│ │ └── AndroidManifest.xml
│ ├── debug
│ │ └── AndroidManifest.xml
│ ├── test
│ │ └── java
│ │ │ └── com
│ │ │ └── github
│ │ │ └── rahul_gill
│ │ │ └── attendance
│ │ │ └── db
│ │ │ └── FutureThingCalculationsTest.kt
│ └── androidTest
│ │ └── java
│ │ └── com
│ │ └── github
│ │ └── rahul_gill
│ │ └── attendance
│ │ └── ScreenGrabTest.kt
├── proguard-rules.pro
└── build.gradle.kts
├── .github
├── FUNDING.yml
├── workflows
│ ├── gradlew-validation.yml
│ ├── android-build.yml
│ └── pre-merge.yml
└── ISSUE_TEMPLATE
│ └── bug_report.md
├── Gemfile
├── fastlane
├── metadata
│ └── android
│ │ ├── en-US
│ │ ├── title.txt
│ │ ├── short_description.txt
│ │ ├── images
│ │ │ ├── icon.png
│ │ │ └── phoneScreenshots
│ │ │ │ ├── 5_settings_1715112573323.png
│ │ │ │ ├── 4_create_course_1715112572076.png
│ │ │ │ ├── 3_course_details_1715112569747.png
│ │ │ │ ├── 1_main_screen_today_1715112567124.png
│ │ │ │ └── 2_main_screen_overall_courses_1715112568781.png
│ │ └── full_description.txt
│ │ └── screenshots.html
├── Appfile
├── Screengrabfile
└── Fastfile
├── .idea
├── .gitignore
├── kotlinc.xml
├── AndroidProjectSystem.xml
├── migrations.xml
├── deploymentTargetSelector.xml
├── androidTestResultsUserPreferences.xml
├── runConfigurations.xml
└── inspectionProfiles
│ └── Project_Default.xml
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── config
└── detekt
│ ├── baseline.xml
│ └── argsfile
├── gradle.properties
├── settings.gradle.kts
├── README.md
├── gradlew.bat
├── .gitignore
├── gradlew
└── Gemfile.lock
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: rahul-gill
2 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | gem "fastlane"
4 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/title.txt:
--------------------------------------------------------------------------------
1 | Self Attendance Tracker
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/app/keystore.jks:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rahul-gill/Self-Attendance-Tracker/HEAD/app/keystore.jks
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/short_description.txt:
--------------------------------------------------------------------------------
1 | Attendance Tracker for students with focus on UI and usability
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rahul-gill/Self-Attendance-Tracker/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rahul-gill/Self-Attendance-Tracker/HEAD/app/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/app/src/main/res/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rahul-gill/Self-Attendance-Tracker/HEAD/app/src/main/res/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rahul-gill/Self-Attendance-Tracker/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rahul-gill/Self-Attendance-Tracker/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rahul-gill/Self-Attendance-Tracker/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rahul-gill/Self-Attendance-Tracker/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rahul-gill/Self-Attendance-Tracker/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rahul-gill/Self-Attendance-Tracker/HEAD/fastlane/metadata/android/en-US/images/icon.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rahul-gill/Self-Attendance-Tracker/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rahul-gill/Self-Attendance-Tracker/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rahul-gill/Self-Attendance-Tracker/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rahul-gill/Self-Attendance-Tracker/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rahul-gill/Self-Attendance-Tracker/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rahul-gill/Self-Attendance-Tracker/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rahul-gill/Self-Attendance-Tracker/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rahul-gill/Self-Attendance-Tracker/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #F5E893
4 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rahul-gill/Self-Attendance-Tracker/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rahul-gill/Self-Attendance-Tracker/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/values/theme.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/fastlane/Appfile:
--------------------------------------------------------------------------------
1 | json_key_file("") # Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one
2 | package_name("com.github.rahul_gill.attendance") # e.g. com.krausefx.app
3 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/5_settings_1715112573323.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rahul-gill/Self-Attendance-Tracker/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/5_settings_1715112573323.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/4_create_course_1715112572076.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rahul-gill/Self-Attendance-Tracker/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/4_create_course_1715112572076.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/3_course_details_1715112569747.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rahul-gill/Self-Attendance-Tracker/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/3_course_details_1715112569747.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/1_main_screen_today_1715112567124.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rahul-gill/Self-Attendance-Tracker/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/1_main_screen_today_1715112567124.png
--------------------------------------------------------------------------------
/.idea/AndroidProjectSystem.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/config/detekt/baseline.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | LargeClass:UnusedPrivateMemberSpec.kt$UnusedPrivateMemberSpec : Spek
6 |
7 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/2_main_screen_overall_courses_1715112568781.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rahul-gill/Self-Attendance-Tracker/HEAD/fastlane/metadata/android/en-US/images/phoneScreenshots/2_main_screen_overall_courses_1715112568781.png
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sun May 21 14:24:36 IST 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/.idea/migrations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | -keep class com.github.rahul_gill.attendance.db.CourseDetailsOverallItem
2 | -keep class java.time.DayOfWeek
3 | -keep class java.time.LocalTime
4 | -keep class com.github.rahul_gill.attendance.db.TodayCourseItem
5 | -keep class com.github.rahul_gill.attendance.db.ClassDetail
6 | -keep class com.github.rahul_gill.attendance.db.ExtraClassDetails
--------------------------------------------------------------------------------
/.idea/deploymentTargetSelector.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/baseline_add_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/baseline_warning_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/config/detekt/argsfile:
--------------------------------------------------------------------------------
1 | -i
2 | .
3 | -b
4 | ./config/detekt/baseline.xml
5 | -ex
6 | **/resources/**,**/detekt*/build/**,**/buildSrc/build/**
7 | --build-upon-default-config
8 | -c
9 | ./config/detekt/detekt.yml
10 | -r
11 | html:./build/detekt-report.html
12 | -r
13 | xml:./build/detekt-report.xml
14 | -r
15 | txt:./build/detekt-report.txt
16 | -p
17 | ./build/detekt-formatting.jar
--------------------------------------------------------------------------------
/app/src/main/res/drawable/baseline_delete_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/rahul_gill/attendance/util/Constants.kt:
--------------------------------------------------------------------------------
1 | package com.github.rahul_gill.attendance.util
2 |
3 | object Constants {
4 | const val GITHUB_APP_LINK = "https://github.com/rahul-gill/Self-Attendace-Tracker"
5 |
6 | const val GITHUB_USER_LINK = "https://github.com/rahul-gill"
7 |
8 | const val PRIVACY_POLICY_LINK = "https://rahul-gill.github.io/self-attendance-tracker/privacy-policy"
9 |
10 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/baseline_arrow_back_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/baseline_keyboard_arrow_left_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/baseline_keyboard_arrow_right_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx1536m -XX:+UseParallelGC
2 | kotlin.code.style=official
3 | android.useAndroidX=true
4 | #android.enableJetifier=true
5 | org.gradle.daemon=true
6 | org.gradle.caching=true
7 | org.gradle.parallel=true
8 | org.gradle.caching.debug=true
9 | org.gradle.warning.mode=summary
10 | org.gradle.configureondemand=true
11 | #android.enableR8=true
12 | #kapt.use.worker.api=false
13 | #kotlin.incremental=false
14 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/baseline_privacy_tip_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.github/workflows/gradlew-validation.yml:
--------------------------------------------------------------------------------
1 | name: Validate Gradle Wrapper
2 | on:
3 | push:
4 | branches:
5 | - master
6 | pull_request:
7 | branches:
8 | - '*'
9 |
10 | jobs:
11 | validation:
12 | name: Validation
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: Checkout latest code
16 | uses: actions/checkout@v2
17 | - name: Validate Gradle Wrapper
18 | uses: gradle/wrapper-validation-action@v1
--------------------------------------------------------------------------------
/app/src/main/res/drawable/baseline_person_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/baseline_contrast_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/baseline_edit_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | gradlePluginPortal()
4 | google()
5 | mavenCentral()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | maven { setUrl("https://jitpack.io") }
14 | }
15 | }
16 |
17 | rootProject.name = "Attendance Tracker"
18 | include(":app")
19 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/baseline_calendar_today_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/baseline_table_chart_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/baseline_save_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/full_description.txt:
--------------------------------------------------------------------------------
1 | Using Self Attendance Tracker, students can track their class attendance on their own. They can
2 | 1. See the classes they have to attend today
3 | 2. List of course for which attendance is being tracked and see presents, absents and cancelled classes per course
4 | 3. Create schedule for the week so that these schedule classes are repeated weekly
5 | 4. Create extra classes that are additional to the weekly schedule classes
6 | 5. See marked attendance record for a specific course
--------------------------------------------------------------------------------
/app/src/main/java/com/github/rahul_gill/attendance/db/CourseDetailsOverallItem.kt:
--------------------------------------------------------------------------------
1 | package com.github.rahul_gill.attendance.db
2 |
3 | import android.os.Parcelable
4 | import kotlinx.parcelize.Parcelize
5 |
6 | @Parcelize
7 | data class CourseDetailsOverallItem(
8 | val courseId: Long,
9 | val courseName: String,
10 | val requiredAttendance: Double,
11 | val currentAttendancePercentage: Double,
12 | val presents: Int = 0,
13 | val absents: Int = 0,
14 | val cancels: Int = 0,
15 | val unsets: Int = 0
16 | ) : Parcelable
--------------------------------------------------------------------------------
/app/src/main/res/drawable/baseline_colorize_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/fastlane/Screengrabfile:
--------------------------------------------------------------------------------
1 | # remove the leading '#' to uncomment lines
2 |
3 | # app_package_name('your.app.package')
4 | # use_tests_in_packages(['your.screenshot.tests.package'])
5 | app_apk_path('app/build/outputs/apk/debug/app-debug.apk')
6 | tests_apk_path('app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk')
7 |
8 | locales(['en-US'])
9 |
10 | # clear all previously generated screenshots in your local output directory before creating new ones
11 | clear_previous_screenshots(true)
12 |
13 | # For more information about all available options run
14 | # fastlane screengrab --help
15 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/baseline_access_time_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/rahul_gill/attendance/db/ExtraClassDetails.kt:
--------------------------------------------------------------------------------
1 | package com.github.rahul_gill.attendance.db
2 |
3 | import android.os.Parcelable
4 | import com.github.rahulgill.attendance.ExtraClasses
5 | import kotlinx.parcelize.Parcelize
6 | import java.time.LocalDate
7 | import java.time.LocalTime
8 |
9 | @Parcelize
10 | data class ExtraClassDetails(
11 | public val extraClassId: Long,
12 | public val courseId: Long,
13 | public val date: LocalDate,
14 | public val startTime: LocalTime,
15 | public val endTime: LocalTime,
16 | public val classStatus: CourseClassStatus,
17 | ) : Parcelable
--------------------------------------------------------------------------------
/app/src/main/java/com/github/rahul_gill/attendance/db/ClassDetail.kt:
--------------------------------------------------------------------------------
1 | package com.github.rahul_gill.attendance.db
2 |
3 | import android.os.Parcelable
4 | import kotlinx.parcelize.Parcelize
5 | import java.time.DayOfWeek
6 | import java.time.LocalDate
7 | import java.time.LocalTime
8 |
9 | @Parcelize
10 | data class ClassDetail constructor(
11 | val dayOfWeek: DayOfWeek = LocalDate.now().dayOfWeek,
12 | val startTime: LocalTime = LocalTime.now().withMinute(0),
13 | val endTime: LocalTime = startTime.plusHours(1),
14 | val scheduleId: Long? = null,
15 | val includedInSchedule: Boolean = true
16 | ) : Parcelable
--------------------------------------------------------------------------------
/app/src/main/res/drawable/baseline_format_color_fill_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/rahul_gill/attendance/util/ApplicationContextInitializer.kt:
--------------------------------------------------------------------------------
1 | package com.github.rahul_gill.attendance.util
2 |
3 | import android.content.Context
4 | import android.util.Log
5 | import androidx.startup.Initializer
6 |
7 |
8 | private var appContext: Context? = null
9 |
10 | val applicationContextGlobal
11 | get() = appContext!!
12 |
13 |
14 | internal class ApplicationContextInitializer : Initializer {
15 | override fun create(context: Context): Context {
16 | context.applicationContext.also { appContext = it }
17 | Log.i("ApplicationContextInitializer", "init done")
18 | return context.applicationContext
19 | }
20 |
21 | override fun dependencies(): List>> = emptyList()
22 | }
23 |
24 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/rahul_gill/attendance/db/ExtraClassTimings.kt:
--------------------------------------------------------------------------------
1 | package com.github.rahul_gill.attendance.db
2 |
3 | import android.os.Parcelable
4 | import kotlinx.parcelize.Parcelize
5 | import java.time.LocalDate
6 | import java.time.LocalTime
7 |
8 |
9 | @Parcelize
10 | data class ExtraClassTimings(
11 | val date: LocalDate,
12 | val startTime: LocalTime,
13 | val endTime: LocalTime,
14 | ) : Parcelable{
15 | companion object{
16 | fun defaultTimeAdjusted(): ExtraClassTimings {
17 | val start = LocalTime.now().withMinute(0)
18 | return ExtraClassTimings(
19 | date = LocalDate.now(),
20 | startTime = start,
21 | endTime = start.plusHours(1)
22 | )
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/.idea/androidTestResultsUserPreferences.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/app/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
14 |
15 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: "[BUG]: Title goes here"
5 | labels: bug
6 | ---
7 |
8 | **Description**
9 | A clear and concise description of what the bug is.
10 |
11 | **To Reproduce**
12 | Steps to reproduce the behaviour:
13 | 1. Go to '...'
14 | 2. Click on '....'
15 | 3. Scroll down to '....'
16 | 4. See the error
17 |
18 | **Expected behaviour**
19 | A clear and concise description of what you expected to happen.
20 |
21 | **Screenshots**
22 | If applicable, add screenshots to help explain your problem.
23 |
24 | **Smartphone (please complete the following information):**
25 | - Device: [e.g. Pixel 3]
26 | - OS: [e.g. Android10]
27 | - Version [e.g. 30]
28 |
29 | **Additional context**
30 | Add any other context about the problem here.
31 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/baseline_color_lens_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/rahul_gill/attendance/db/CourseClassStatus.kt:
--------------------------------------------------------------------------------
1 | package com.github.rahul_gill.attendance.db
2 |
3 | import com.github.rahul_gill.attendance.R
4 |
5 | enum class CourseClassStatus {
6 | Present,
7 | Absent,
8 | Cancelled,
9 | Unset;
10 |
11 | override fun toString() = when (this) {
12 | Present -> "Present"
13 | Absent -> "Absent"
14 | Cancelled -> "Cancelled"
15 | Unset -> "Unset"
16 |
17 | }
18 |
19 | companion object {
20 | fun fromString(str: String) = when (str) {
21 | "Present" -> Present
22 | "Absent" -> Absent
23 | "Cancelled" -> Cancelled
24 | "Unset" -> Unset
25 | else -> throw IllegalArgumentException("Status can only be either one of these: Present, Absent, Cancelled, Unset")
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/baseline_palette_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.github/workflows/android-build.yml:
--------------------------------------------------------------------------------
1 | name: Android CI debug annd release build
2 | on:
3 | workflow_dispatch:
4 | push:
5 | branches:
6 | - master
7 |
8 | env:
9 | SIGN_RELEASE_WITH_DEBUG_KEY_CI: 1
10 |
11 | jobs:
12 | build:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: Checkout Repo
16 | uses: actions/checkout@v2
17 | - name: set up JDK 17
18 | uses: actions/setup-java@v1
19 | with:
20 | java-version: 17
21 | - name: Set Gradle as executable
22 | run: chmod +x ./gradlew
23 | - name: Build Debug and Release APK
24 | run: ./gradlew assembleDebug assembleRelease
25 | - name: Output debug apk
26 | uses: actions/upload-artifact@v3
27 | with:
28 | name: debug-apk
29 | path: app/build/outputs/apk/debug/*.apk
30 | - name: Output release apks
31 | uses: actions/upload-artifact@v3
32 | with:
33 | name: release-apk
34 | path: app/build/outputs/apk/release/*.apk
35 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/rahul_gill/attendance/AttendanceApp.kt:
--------------------------------------------------------------------------------
1 | package com.github.rahul_gill.attendance
2 |
3 | import android.app.Application
4 | import android.os.StrictMode
5 | import android.os.StrictMode.ThreadPolicy
6 | import android.os.StrictMode.VmPolicy
7 | import timber.log.Timber
8 |
9 |
10 | class AttendanceApp : Application() {
11 |
12 | override fun onCreate() {
13 | if (BuildConfig.DEBUG) {
14 | StrictMode.setThreadPolicy(
15 | ThreadPolicy.Builder()
16 | .detectDiskReads()
17 | .detectDiskWrites()
18 | .detectAll()
19 | .penaltyLog()
20 | .build()
21 | )
22 | StrictMode.setVmPolicy(
23 | VmPolicy.Builder()
24 | .detectLeakedSqlLiteObjects()
25 | .detectLeakedClosableObjects()
26 | .penaltyLog()
27 | .build()
28 | )
29 | }
30 | super.onCreate()
31 | Timber.plant(Timber.DebugTree())
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/rahul_gill/attendance/util/DateTimeFormats.kt:
--------------------------------------------------------------------------------
1 | package com.github.rahul_gill.attendance.util
2 |
3 | import com.github.rahul_gill.attendance.prefs.PreferenceManager
4 | import java.time.LocalDate
5 | import java.time.format.DateTimeFormatter
6 |
7 | val timeFormatter: DateTimeFormatter
8 | get() = DateTimeFormatter.ofPattern(PreferenceManager.defaultTimeFormatPref.value)
9 |
10 | val dateFormatter: DateTimeFormatter
11 | get() = DateTimeFormatter.ofPattern(PreferenceManager.defaultDateFormatPref.value)
12 |
13 | fun formatWeek(startDate: LocalDate): String {
14 | val endDate = startDate.plusWeeks(1)
15 | return when {
16 | startDate.year == endDate.year && startDate.month == endDate.month ->
17 | startDate.dayOfMonth.toString() + " - " + endDate.format(DateTimeFormatter.ofPattern("d MMM, yyyy"))
18 |
19 | startDate.year == endDate.year ->
20 | startDate.format(DateTimeFormatter.ofPattern("d MMM - ")) + endDate.format(dateFormatter)
21 |
22 | else ->
23 | startDate.format(dateFormatter) + " - " + endDate.format(dateFormatter)
24 | }
25 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/github.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/.github/workflows/pre-merge.yml:
--------------------------------------------------------------------------------
1 | name: Pre Merge Checks
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 | branches:
9 | - '*'
10 |
11 | jobs:
12 | gradle:
13 | strategy:
14 | matrix:
15 | os: [ubuntu-latest, macos-latest, windows-latest]
16 | runs-on: ${{ matrix.os }}
17 | if: ${{ !contains(github.event.head_commit.message, 'ci skip') }}
18 | steps:
19 | - name: Checkout Repo
20 | uses: actions/checkout@v2
21 | - name: set up JDK 17
22 | uses: actions/setup-java@v1
23 | with:
24 | java-version: 17
25 | - name: Unit tests
26 | run: bash ./gradlew test --stacktrace
27 | - name: Cache Gradle Caches
28 | uses: actions/cache@v1
29 | with:
30 | path: ~/.gradle/caches/
31 | key: cache-gradle-cache
32 | - name: Cache Gradle Wrapper
33 | uses: actions/cache@v1
34 | with:
35 | path: ~/.gradle/wrapper/
36 | key: cache-gradle-wrapper
37 | - name: Run Gradle tasks
38 | run: ./gradlew build --continue
39 | - name: Stop Gradle
40 | run: ./gradlew --stop
41 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/baseline_settings_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/rahul_gill/attendance/db/SqldelightAdapters.kt:
--------------------------------------------------------------------------------
1 | package com.github.rahul_gill.attendance.db
2 |
3 | import app.cash.sqldelight.ColumnAdapter
4 | import java.time.DayOfWeek
5 | import java.time.LocalDate
6 | import java.time.LocalTime
7 | import java.time.format.DateTimeFormatter
8 |
9 | object LocalDateAdapter : ColumnAdapter {
10 | override fun decode(databaseValue: String): LocalDate = LocalDate.parse(databaseValue)
11 | override fun encode(value: LocalDate): String = value.format(DateTimeFormatter.ISO_DATE)
12 | }
13 |
14 | object LocalTimeAdapter : ColumnAdapter {
15 | override fun decode(databaseValue: String): LocalTime = LocalTime.parse(databaseValue)
16 | override fun encode(value: LocalTime): String = value.format(DateTimeFormatter.ISO_TIME)
17 | }
18 |
19 | object DayOfWeekAdapter : ColumnAdapter {
20 | override fun decode(databaseValue: Long): DayOfWeek {
21 | return if(databaseValue == 0L) DayOfWeek.SUNDAY
22 | else DayOfWeek.of(databaseValue.toInt())
23 | }
24 | override fun encode(value: DayOfWeek): Long {
25 | return (value.value % 7).toLong()
26 | }
27 | }
--------------------------------------------------------------------------------
/fastlane/Fastfile:
--------------------------------------------------------------------------------
1 | # This file contains the fastlane.tools configuration
2 | # You can find the documentation at https://docs.fastlane.tools
3 | #
4 | # For a list of all available actions, check out
5 | #
6 | # https://docs.fastlane.tools/actions
7 | #
8 | # For a list of all available plugins, check out
9 | #
10 | # https://docs.fastlane.tools/plugins/available-plugins
11 | #
12 |
13 | # Uncomment the line if you want fastlane to automatically update itself
14 | # update_fastlane
15 |
16 | default_platform(:android)
17 |
18 | platform :android do
19 | desc "Runs all the tests"
20 | lane :test do
21 | gradle(task: "test")
22 | end
23 |
24 | desc "Submit a new Beta Build to Crashlytics Beta"
25 | lane :beta do
26 | gradle(task: "clean assembleRelease")
27 | crashlytics
28 | # sh "your_script.sh"
29 | # You can also use other beta testing services here
30 | end
31 |
32 | desc "Deploy a new version to the Google Play"
33 | lane :deploy do
34 | gradle(task: "clean assembleRelease")
35 | upload_to_play_store
36 | end
37 |
38 |
39 | desc "Build debug and test APK for screenshots"
40 | lane :build_and_screengrab do
41 | build_android_app(
42 | task: 'assemble',
43 | build_type: 'Debug'
44 | )
45 | build_android_app(
46 | task: 'assemble',
47 | build_type: 'AndroidTest'
48 | )
49 | screengrab()
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/app/src/main/sqldelight/migrations/1.sqm:
--------------------------------------------------------------------------------
1 | import com.github.rahul_gill.attendance.db.CourseClassStatus;
2 | import java.time.DayOfWeek;
3 | import java.time.LocalDate;
4 | import java.time.LocalTime;
5 |
6 | CREATE TABLE Attendance_temp (
7 | attendanceId INTEGER PRIMARY KEY AUTOINCREMENT,
8 | scheduleId INTEGER,
9 | courseId INTEGER,
10 | classStatus TEXT NOT NULL,
11 | date TEXT NOT NULL
12 | );
13 |
14 | INSERT INTO Attendance_temp(scheduleId, courseId, classStatus, date)
15 | SELECT Attendance.scheduleId, (SELECT Schedule.courseId FROM Schedule WHERE Schedule.scheduleId = Attendance.scheduleId), Attendance.classStatus, Attendance.date
16 | FROM Attendance;
17 |
18 | DROP TABLE Attendance;
19 |
20 | CREATE TABLE Attendance (
21 | attendanceId INTEGER PRIMARY KEY AUTOINCREMENT,
22 | scheduleId INTEGER,
23 | courseId INTEGER,
24 | classStatus TEXT AS CourseClassStatus NOT NULL,
25 | date TEXT AS LocalDate NOT NULL,
26 | CONSTRAINT fk_schedule
27 | FOREIGN KEY (scheduleId)
28 | REFERENCES Schedule (scheduleId)
29 | ON DELETE CASCADE,
30 | CONSTRAINT fk_course
31 | FOREIGN KEY (courseId)
32 | REFERENCES Course (courseId)
33 | ON DELETE CASCADE
34 | );
35 |
36 | INSERT INTO Attendance(attendanceId, scheduleId, courseId, classStatus, date)
37 | SELECT tmp.attendanceId, tmp.scheduleId, tmp.classStatus, tmp.classStatus, tmp.date FROM Attendance_temp tmp;
38 |
39 | DROP TABLE Attendance_temp;
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
29 |
30 |
36 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
18 |
22 |
23 |
24 |
25 |
26 |
27 |
32 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/rahul_gill/attendance/db/FutureThingCalculations.kt:
--------------------------------------------------------------------------------
1 | package com.github.rahul_gill.attendance.db
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.ui.res.stringResource
5 | import com.github.rahul_gill.attendance.R
6 | import kotlin.math.max
7 | import kotlin.math.min
8 |
9 |
10 | object FutureThingCalculations {
11 | fun allowedAbsentsInFuture(presents: Int, absents: Int, requiredPercentage: Int): Int {
12 | val exp =
13 | ((100 - requiredPercentage) * presents - requiredPercentage * absents) / requiredPercentage
14 | return max(0, exp)
15 | }
16 |
17 | fun neededPresentsInFuture(presents: Int, absents: Int, requiredPercentage: Int): Int {
18 | val exp =
19 | (requiredPercentage * absents - (100 - requiredPercentage) * presents) / (100 - requiredPercentage)
20 | return max(0, exp)
21 | }
22 |
23 | @Composable
24 | fun getMessageForFuture(presents: Int, absents: Int, requiredPercentage: Int): String {
25 | val presentsNeeded = neededPresentsInFuture(presents, absents, requiredPercentage)
26 | val absentsAllowed = allowedAbsentsInFuture(presents, absents, requiredPercentage)
27 | return when {
28 | absentsAllowed != 0 -> stringResource(
29 | id = R.string.can_be_absent_in_n_classes,
30 | absentsAllowed
31 | )
32 | presentsNeeded != 0 -> stringResource(
33 | id = R.string.need_to_attend_n_more_classes,
34 | presentsNeeded
35 | )
36 | else -> stringResource(id = R.string.cannot_miss_next_class)
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Student Attendance Tracker
2 |
3 | Using Self Attendance Tracker, students can track their class attendance on their own. They can:
4 | 1. See the classes they have to attend today
5 | 2. List of course for which attendance is being tracked and see presents, absents and cancelled classes per course
6 | 3. Create schedule for the week so that these schedule classes are repeated weekly
7 | 4. Create extra classes that are additional to the weekly schedule classes
8 | 5. See marked attendance record for a specific course
9 |
10 |
11 |
12 | [
](https://play.google.com/store/apps/details?id=com.github.rahul_gill.attendance) [
](https://f-droid.org/packages/com.github.rahul_gill.attendance/)
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/rahul_gill/attendance/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.github.rahul_gill.attendance
2 |
3 | import android.graphics.Color
4 | import android.os.Bundle
5 | import androidx.activity.ComponentActivity
6 | import androidx.activity.SystemBarStyle
7 | import androidx.activity.compose.setContent
8 | import androidx.activity.enableEdgeToEdge
9 | import androidx.compose.foundation.layout.fillMaxSize
10 | import androidx.compose.material3.MaterialTheme
11 | import androidx.compose.material3.Surface
12 | import androidx.compose.ui.Modifier
13 | import com.github.rahul_gill.attendance.prefs.PreferenceManager
14 | import com.github.rahul_gill.attendance.ui.RootNavHost
15 | import com.github.rahul_gill.attendance.ui.comps.AttendanceAppTheme
16 | import com.github.rahul_gill.attendance.ui.comps.ColorSchemeType
17 |
18 | class MainActivity : ComponentActivity() {
19 | override fun onCreate(savedInstanceState: Bundle?) {
20 | super.onCreate(savedInstanceState)
21 | enableEdgeToEdge(
22 | statusBarStyle = SystemBarStyle.light(
23 | Color.TRANSPARENT, Color.TRANSPARENT
24 | ),
25 | navigationBarStyle = SystemBarStyle.light(
26 | Color.TRANSPARENT, Color.TRANSPARENT
27 | )
28 | )
29 | setContent {
30 | val followSystemColor = PreferenceManager.followSystemColors.asState()
31 | val seedColor = PreferenceManager.colorSchemeSeed.asState()
32 | val theme = PreferenceManager.themeConfig.asState()
33 | val darkThemeType = PreferenceManager.darkThemeType.asState()
34 | AttendanceAppTheme(
35 | colorSchemeType = if (followSystemColor.value) ColorSchemeType.Dynamic else ColorSchemeType.WithSeed(
36 | seedColor.value
37 | ),
38 | themeConfig = theme.value,
39 | darkThemeType = darkThemeType.value
40 | ) {
41 | Surface(
42 | modifier = Modifier
43 | .fillMaxSize(),
44 | color = MaterialTheme.colorScheme.background
45 | ) {
46 | RootNavHost()
47 | }
48 | }
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/rahul_gill/attendance/prefs/PreferenceManager.kt:
--------------------------------------------------------------------------------
1 | package com.github.rahul_gill.attendance.prefs
2 |
3 | import androidx.compose.ui.graphics.Color
4 | import com.github.rahul_gill.attendance.ui.comps.DarkThemeType
5 | import com.github.rahul_gill.attendance.ui.comps.ThemeConfig
6 | import com.github.rahul_gill.attendance.util.BooleanPreference
7 | import com.github.rahul_gill.attendance.util.IntPreference
8 | import com.github.rahul_gill.attendance.util.LongPreference
9 | import com.github.rahul_gill.attendance.util.StringPreference
10 | import com.github.rahul_gill.attendance.util.customPreference
11 | import com.github.rahul_gill.attendance.util.enumPreference
12 |
13 | const val DefaultTimeFormat = "hh:mm a"
14 | const val DefaultDateFormat = "d MMM, yyyy"
15 | val DefaultColorSchemeSeed = Color.Green
16 |
17 | enum class UnsetClassesBehavior {
18 | ConsiderPresent,
19 | ConsiderAbsent,
20 | None
21 | }
22 |
23 | object PreferenceManager {
24 | val themeConfig = enumPreference(
25 | key = "theme_config",
26 | defaultValue = ThemeConfig.FollowSystem
27 | )
28 | val darkThemeType = enumPreference(
29 | key = "dark_theme_type",
30 | defaultValue = DarkThemeType.Dark
31 | )
32 | val unsetClassesBehavior = enumPreference(
33 | key = "unset_classes_behaviour",
34 | defaultValue = UnsetClassesBehavior.None
35 | )
36 | val followSystemColors =
37 | BooleanPreference(key = "follow_system_colors", defaultValue = false)
38 | val colorSchemeSeed = customPreference(
39 | backingPref = LongPreference("color_scheme_type", 0),
40 | defaultValue = DefaultColorSchemeSeed,
41 | serialize = { color -> color.value.toLong() },
42 | deserialize = { if (it == 0L) DefaultColorSchemeSeed else Color(it.toULong()) }
43 | )
44 |
45 | val defaultHomeTabPref =
46 | IntPreference(key = "default_home_tab", defaultValue = 0)
47 | val defaultTimeFormatPref =
48 | StringPreference(
49 | key = "time_format",
50 | defaultValue = DefaultTimeFormat
51 | )
52 | val defaultDateFormatPref =
53 | StringPreference(
54 | key = "date_format",
55 | defaultValue = DefaultDateFormat
56 | )
57 | }
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/rahul_gill/attendance/db/TodayCourseItem.kt:
--------------------------------------------------------------------------------
1 | package com.github.rahul_gill.attendance.db
2 |
3 | import android.os.Parcelable
4 | import com.github.rahulgill.attendance.MarkedAttendancesForCourse
5 | import kotlinx.parcelize.Parcelize
6 | import java.time.LocalDate
7 | import java.time.LocalTime
8 |
9 | sealed interface AttendanceRecordHybrid : Parcelable {
10 | val courseName: String
11 | val courseId: Long
12 | val startTime: LocalTime
13 | val endTime: LocalTime
14 | val classStatus: CourseClassStatus
15 | val date: LocalDate
16 |
17 |
18 | /**
19 | * Attendance id null when we're just showing a record in today item but the item is not yet created
20 | * scheduledId might be null if schedule is deleted but the attendance record is created
21 | */
22 | @Parcelize
23 | class ScheduledClass(
24 | val attendanceId: Long?,
25 | val scheduleId: Long?,
26 | override val courseId: Long,
27 | override val courseName: String,
28 | override val startTime: LocalTime,
29 | override val endTime: LocalTime,
30 | override val classStatus: CourseClassStatus,
31 | override val date: LocalDate,
32 | ) : AttendanceRecordHybrid
33 |
34 | @Parcelize
35 | class ExtraClass(
36 | val extraClassId: Long,
37 | override val courseId: Long,
38 | override val courseName: String,
39 | override val startTime: LocalTime,
40 | override val endTime: LocalTime,
41 | override val classStatus: CourseClassStatus,
42 | override val date: LocalDate,
43 | ) : AttendanceRecordHybrid
44 | }
45 |
46 | @Parcelize
47 | data class TodayCourseItem(
48 | val attendanceIdOrExtraClassId: Long,
49 | val courseName: String,
50 | val startTime: LocalTime,
51 | val endTime: LocalTime,
52 | val classStatus: CourseClassStatus,
53 | val isExtraClass: Boolean = false,
54 | val date: LocalDate? = null
55 | ) : Parcelable {
56 |
57 |
58 | companion object {
59 | fun fromMarkedAttendancesForCourse(
60 | item: MarkedAttendancesForCourse, courseName: String
61 | ): TodayCourseItem {
62 | return TodayCourseItem(
63 | attendanceIdOrExtraClassId = item.entityId,
64 | isExtraClass = item.isExtraCLass != 0L,
65 | date = item.date,
66 | startTime = item.startTime,
67 | endTime = item.endTime,
68 | courseName = courseName,
69 | classStatus = item.classStatus
70 | )
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/rahul_gill/attendance/ui/comps/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.github.rahul_gill.attendance.ui.comps
2 |
3 | import android.app.Activity
4 | import android.os.Build
5 | import androidx.compose.foundation.isSystemInDarkTheme
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.material3.dynamicDarkColorScheme
8 | import androidx.compose.material3.dynamicLightColorScheme
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.runtime.SideEffect
11 | import androidx.compose.runtime.remember
12 | import androidx.compose.ui.graphics.Color
13 | import androidx.compose.ui.graphics.toArgb
14 | import androidx.compose.ui.platform.LocalContext
15 | import androidx.compose.ui.platform.LocalView
16 | import androidx.core.view.WindowCompat
17 | import com.github.rahul_gill.attendance.prefs.DefaultColorSchemeSeed
18 | import com.materialkolor.dynamicColorScheme
19 |
20 | enum class DarkThemeType {
21 | Dark, Black
22 | }
23 |
24 | enum class ThemeConfig {
25 | FollowSystem, Light, Dark
26 | }
27 |
28 | sealed interface ColorSchemeType {
29 | class WithSeed(val seed: Color = Color.Blue) : ColorSchemeType
30 | data object Dynamic : ColorSchemeType
31 | }
32 |
33 |
34 | @Composable
35 | fun AttendanceAppTheme(
36 | colorSchemeType: ColorSchemeType = ColorSchemeType.Dynamic,
37 | themeConfig: ThemeConfig = ThemeConfig.FollowSystem,
38 | darkThemeType: DarkThemeType = DarkThemeType.Dark,
39 | content: @Composable () -> Unit
40 | ) {
41 | val context = LocalContext.current
42 | val isDarkTheme =
43 | themeConfig == ThemeConfig.Dark || (themeConfig == ThemeConfig.FollowSystem && isSystemInDarkTheme())
44 | val colorScheme = remember(colorSchemeType, isDarkTheme, darkThemeType) {
45 | val scheme = when (colorSchemeType) {
46 | ColorSchemeType.Dynamic -> {
47 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
48 | if (isDarkTheme)
49 | dynamicDarkColorScheme(context)
50 | else
51 | dynamicLightColorScheme(context)
52 | } else {
53 | dynamicColorScheme(DefaultColorSchemeSeed, isDarkTheme)
54 | }
55 | }
56 |
57 | is ColorSchemeType.WithSeed -> {
58 | dynamicColorScheme(colorSchemeType.seed, isDarkTheme)
59 | }
60 | }
61 | scheme.copy(
62 | background = if (isDarkTheme && darkThemeType == DarkThemeType.Black) Color.Black else scheme.background,
63 | surface = if (isDarkTheme && darkThemeType == DarkThemeType.Black) Color.Black else scheme.surface
64 | )
65 | }
66 |
67 | val view = LocalView.current
68 | if (!view.isInEditMode) {
69 | SideEffect {
70 | val window = (view.context as Activity).window
71 | window.statusBarColor = Color.Transparent.toArgb()
72 | WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars =
73 | !isDarkTheme
74 | }
75 | }
76 |
77 | MaterialTheme(
78 | colorScheme = colorScheme,
79 | content = content
80 | )
81 |
82 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/github/rahul_gill/attendance/db/FutureThingCalculationsTest.kt:
--------------------------------------------------------------------------------
1 | package com.github.rahul_gill.attendance.db
2 |
3 | import org.junit.Test
4 | import org.junit.runner.RunWith
5 | import org.junit.runners.JUnit4
6 | import org.junit.Assert.assertTrue
7 |
8 | @RunWith(JUnit4::class)
9 | class FutureThingCalculationsTest {
10 |
11 | @Test
12 | fun `test some cases and check if future things calculations are correct`() {
13 | var presents = 6
14 | var absents = 0
15 | var requiredPercentage = 75
16 | assertTrue(
17 | FutureThingCalculations.neededPresentsInFuture(
18 | presents,
19 | absents,
20 | requiredPercentage
21 | ) == 0
22 | )
23 | assertTrue(
24 | FutureThingCalculations.allowedAbsentsInFuture(
25 | presents,
26 | absents,
27 | requiredPercentage
28 | ) == 2
29 | )
30 | presents = 6
31 | absents = 1
32 | requiredPercentage = 75
33 | assertTrue(
34 | FutureThingCalculations.neededPresentsInFuture(
35 | presents,
36 | absents,
37 | requiredPercentage
38 | ) == 0
39 | )
40 | assertTrue(
41 | FutureThingCalculations.allowedAbsentsInFuture(
42 | presents,
43 | absents,
44 | requiredPercentage
45 | ) == 1
46 | )
47 | presents = 6
48 | absents = 2
49 | requiredPercentage = 75
50 | assertTrue(
51 | FutureThingCalculations.neededPresentsInFuture(
52 | presents,
53 | absents,
54 | requiredPercentage
55 | ) == 0
56 | )
57 | assertTrue(
58 | FutureThingCalculations.allowedAbsentsInFuture(
59 | presents,
60 | absents,
61 | requiredPercentage
62 | ) == 0
63 | )
64 | presents = 5
65 | absents = 2
66 | requiredPercentage = 75
67 | assertTrue(
68 | FutureThingCalculations.neededPresentsInFuture(
69 | presents,
70 | absents,
71 | requiredPercentage
72 | ) == 1
73 | )
74 | assertTrue(
75 | FutureThingCalculations.allowedAbsentsInFuture(
76 | presents,
77 | absents,
78 | requiredPercentage
79 | ) == 0
80 | )
81 |
82 | presents = 4
83 | absents = 2
84 | requiredPercentage = 75
85 | assertTrue(
86 | FutureThingCalculations.neededPresentsInFuture(
87 | presents,
88 | absents,
89 | requiredPercentage
90 | ) == 2
91 | )
92 | assertTrue(
93 | FutureThingCalculations.allowedAbsentsInFuture(
94 | presents,
95 | absents,
96 | requiredPercentage
97 | ) == 0
98 | )
99 | }
100 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/rahul_gill/attendance/ui/comps/ScheduleItem.kt:
--------------------------------------------------------------------------------
1 | package com.github.rahul_gill.attendance.ui.comps
2 |
3 | import androidx.compose.animation.animateContentSize
4 | import androidx.compose.foundation.layout.Arrangement
5 | import androidx.compose.foundation.layout.Box
6 | import androidx.compose.foundation.layout.Row
7 | import androidx.compose.foundation.layout.Spacer
8 | import androidx.compose.foundation.layout.fillMaxWidth
9 | import androidx.compose.foundation.layout.height
10 | import androidx.compose.foundation.layout.padding
11 | import androidx.compose.foundation.layout.width
12 | import androidx.compose.foundation.shape.RoundedCornerShape
13 | import androidx.compose.material.icons.Icons
14 | import androidx.compose.material.icons.filled.Close
15 | import androidx.compose.material3.Icon
16 | import androidx.compose.material3.IconButton
17 | import androidx.compose.material3.MaterialTheme
18 | import androidx.compose.material3.OutlinedCard
19 | import androidx.compose.material3.Surface
20 | import androidx.compose.material3.Text
21 | import androidx.compose.material3.minimumInteractiveComponentSize
22 | import androidx.compose.runtime.Composable
23 | import androidx.compose.ui.Alignment
24 | import androidx.compose.ui.Modifier
25 | import androidx.compose.ui.res.stringResource
26 | import androidx.compose.ui.unit.dp
27 | import com.github.rahul_gill.attendance.R
28 | import com.github.rahul_gill.attendance.db.ClassDetail
29 | import com.github.rahul_gill.attendance.util.timeFormatter
30 |
31 | @Composable
32 | fun ScheduleItem(
33 | item: ClassDetail,
34 | modifier: Modifier = Modifier,
35 | onClick: () -> Unit,
36 | popupContent: (@Composable () -> Unit)? = null,
37 | onCloseClick: (() -> Unit)? = null
38 | ) {
39 | OutlinedCard(
40 | onClick = onClick,
41 | modifier = Modifier
42 | .fillMaxWidth()
43 | .animateContentSize()
44 | .then(modifier)
45 | ) {
46 | Row(
47 | Modifier.padding(horizontal = 8.dp),
48 | verticalAlignment = Alignment.CenterVertically
49 | ) {
50 | Text(text = item.dayOfWeek.name)
51 | Spacer(modifier = Modifier.weight(1f))
52 | Text(
53 | text = stringResource(
54 | id = R.string.time_range,
55 | item.startTime.format(timeFormatter),
56 | item.endTime.format(timeFormatter)
57 | )
58 | )
59 | Spacer(modifier = Modifier.width(8.dp))
60 | if (onCloseClick != null) {
61 | IconButton(onClick = onCloseClick) {
62 | Icon(
63 | Icons.Default.Close,
64 | contentDescription = stringResource(id = R.string.remove_class)
65 | )
66 | }
67 | } else {
68 | Box(Modifier.height(48.dp)) {}
69 | }
70 | if (popupContent != null) {
71 | popupContent()
72 | }
73 | }
74 | if(!item.includedInSchedule){
75 | Surface(
76 | modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
77 | shape = RoundedCornerShape(25),
78 | color = MaterialTheme.colorScheme.secondaryContainer
79 | ) {
80 | Text(
81 | modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
82 | text = stringResource(id = R.string.excluded_from_schedule),
83 | color = MaterialTheme.colorScheme.onSecondaryContainer,
84 | style = MaterialTheme.typography.labelMedium
85 | )
86 | }
87 | }
88 | }
89 | }
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.gradle.kotlin.dsl.support.kotlinCompilerOptions
2 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget
3 |
4 | plugins {
5 | id("com.android.application")
6 | kotlin("android")
7 | id("kotlin-parcelize")
8 | id("app.cash.sqldelight")
9 | id("org.jetbrains.kotlin.plugin.compose")
10 | }
11 |
12 | sqldelight {
13 | databases {
14 | create("Database") {
15 | packageName.set("com.github.rahul_gill.attendance")
16 | }
17 | }
18 | }
19 |
20 | object VersionProperties {
21 | private const val versionMajor: Int = 1
22 | private const val versionMinor: Int = 0
23 | private const val versionPatch: Int = 1
24 | private const val versionBuild: Int = 0
25 | const val compileSdk: Int = 36
26 | const val minSdk: Int = 26
27 |
28 | val versionCode
29 | get() = versionMajor * 10000 + versionMinor * 1000 + versionPatch * 100 + versionBuild
30 | val versionName
31 | get() = "$versionMajor.$versionMinor.$versionPatch"
32 | }
33 |
34 | android {
35 | namespace = "com.github.rahul_gill.attendance"
36 | compileSdk = VersionProperties.compileSdk
37 |
38 | defaultConfig {
39 | applicationId = "com.github.rahul_gill.attendance"
40 | minSdk = VersionProperties.minSdk
41 | targetSdk = VersionProperties.compileSdk
42 | versionCode = VersionProperties.versionCode
43 | versionName = VersionProperties.versionName
44 | vectorDrawables.useSupportLibrary = true
45 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
46 | }
47 | sourceSets {
48 | map { it.java.srcDir("src/${it.name}/kotlin") }
49 | }
50 | buildTypes {
51 | getByName("release") {
52 | isMinifyEnabled = true
53 | isShrinkResources = true
54 | proguardFiles(
55 | getDefaultProguardFile("proguard-android-optimize.txt"),
56 | file("proguard-rules.pro")
57 | )
58 | if(!System.getenv("SIGN_RELEASE_WITH_DEBUG_KEY_CI").isNullOrBlank()){
59 | applicationIdSuffix = ".ci"
60 | signingConfig = signingConfigs.getByName("debug")
61 | }
62 | ndk.debugSymbolLevel = "FULL"
63 | }
64 | }
65 | compileOptions {
66 | isCoreLibraryDesugaringEnabled = true
67 | targetCompatibility = JavaVersion.VERSION_17
68 | sourceCompatibility = JavaVersion.VERSION_17
69 | }
70 | kotlin {
71 | compilerOptions {
72 | jvmTarget.set(JvmTarget.JVM_17)
73 | }
74 | }
75 | buildFeatures{
76 | buildConfig = true
77 | compose = true
78 | }
79 |
80 | packaging {
81 | resources {
82 | excludes.add("/META-INF/{AL2.0,LGPL2.1}")
83 | }
84 | }
85 | }
86 |
87 | dependencies {
88 | //basics
89 | implementation("androidx.core:core-ktx:1.17.0")
90 | implementation("com.jakewharton.timber:timber:5.0.1")
91 | implementation("androidx.datastore:datastore-preferences:1.1.7")
92 | implementation("androidx.startup:startup-runtime:1.2.0")
93 | testImplementation("junit:junit:4.13.2")
94 | androidTestImplementation("androidx.test.ext:junit:1.3.0")
95 | androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")
96 |
97 | //sqlDelight
98 | implementation("app.cash.sqldelight:android-driver:2.1.0")
99 | implementation("app.cash.sqldelight:coroutines-extensions:2.1.0")
100 | implementation("app.cash.sqldelight:primitive-adapters:2.1.0")
101 | testImplementation("app.cash.sqldelight:sqlite-driver:2.1.0")
102 | //sugar and water
103 | coreLibraryDesugaring("com.android.tools:desugar_jdk_libs_nio:2.1.5")
104 | //ui thing
105 | implementation (platform("androidx.compose:compose-bom:2025.10.01"))
106 | androidTestImplementation(platform("androidx.compose:compose-bom:2025.10.01"))
107 | implementation ("androidx.compose.material3:material3")
108 | // https://mvnrepository.com/artifact/androidx.compose.material/material-icons-extended
109 | implementation("androidx.compose.material:material-icons-extended:1.7.8")
110 | implementation("androidx.compose.ui:ui-tooling-preview")
111 | debugImplementation("androidx.compose.ui:ui-tooling")
112 | implementation("androidx.activity:activity-compose:1.11.0")
113 | implementation("com.materialkolor:material-kolor:4.0.2")
114 | implementation("com.github.skydoves:colorpicker-compose:1.1.2")
115 | implementation("dev.olshevski.navigation:reimagined:1.5.0")
116 | implementation("androidx.lifecycle:lifecycle-runtime-compose:2.9.4")
117 | androidTestImplementation("tools.fastlane:screengrab:2.1.1")
118 | debugImplementation("androidx.compose.ui:ui-test-manifest")
119 | androidTestImplementation("androidx.compose.ui:ui-test-junit4")
120 |
121 | }
122 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /local.properties
2 | /.idea/caches
3 | /.idea/libraries
4 | /.idea/modules.xml
5 | /.idea/workspace.xml
6 | /.idea/navEditor.xml
7 | /.idea/assetWizardSettings.xml
8 | /.idea/sonarlint
9 | /.idea/deploymentTargetDropDown.xml
10 | /build
11 | /captures
12 | .cxx
13 |
14 | # Created by https://www.gitignore.io/api/kotlin,android,androidstudio
15 | # Edit at https://www.gitignore.io/?templates=kotlin,android,androidstudio
16 |
17 | ### Android ###
18 | # Built application files
19 | *.apk
20 | *.ap_
21 | *.aab
22 |
23 | # Files for the ART/Dalvik VM
24 | *.dex
25 |
26 | # Java class files
27 | *.class
28 |
29 | # Generated files
30 | bin/
31 | gen/
32 | out/
33 | release/
34 |
35 | # Gradle files
36 | .gradle/
37 | build/
38 |
39 | # Local configuration file (sdk path, etc)
40 | local.properties
41 |
42 | # Proguard folder generated by Eclipse
43 | proguard/
44 |
45 | # Log Files
46 | *.log
47 |
48 | # Android Studio Navigation editor temp files
49 | .navigation/
50 |
51 | # Android Studio captures folder
52 | captures/
53 |
54 | # IntelliJ
55 | *.iml
56 | .idea/workspace.xml
57 | .idea/tasks.xml
58 | .idea/gradle.xml
59 | .idea/assetWizardSettings.xml
60 | .idea/dictionaries
61 | .idea/libraries
62 | # Android Studio 3 in .gitignore file.
63 | .idea/caches
64 | .idea/modules.xml
65 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you
66 | .idea/navEditor.xml
67 |
68 | # Keystore files
69 | # Uncomment the following lines if you do not want to check your keystore files in.
70 | #*.jks
71 | #*.keystore
72 |
73 | # External native build folder generated in Android Studio 2.2 and later
74 | .externalNativeBuild
75 |
76 | # Google Services (e.g. APIs or Firebase)
77 | # google-services.json
78 |
79 | # Freeline
80 | freeline.py
81 | freeline/
82 | freeline_project_description.json
83 |
84 | # fastlane
85 | fastlane/report.xml
86 | fastlane/Preview.html
87 | fastlane/screenshots
88 | fastlane/test_output
89 | fastlane/readme.md
90 |
91 | # Version control
92 | vcs.xml
93 |
94 | # lint
95 | lint/intermediates/
96 | lint/generated/
97 | lint/outputs/
98 | lint/tmp/
99 | # lint/reports/
100 |
101 | ### Android Patch ###
102 | gen-external-apklibs
103 | output.json
104 |
105 | # Replacement of .externalNativeBuild directories introduced
106 | # with Android Studio 3.5.
107 | .cxx/
108 |
109 | ### Kotlin ###
110 | # Compiled class file
111 |
112 | # Log file
113 |
114 | # BlueJ files
115 | *.ctxt
116 |
117 | # Mobile Tools for Java (J2ME)
118 | .mtj.tmp/
119 |
120 | # Package Files #
121 | *.jar
122 | *.war
123 | *.nar
124 | *.ear
125 | *.zip
126 | *.tar.gz
127 | *.rar
128 |
129 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
130 | hs_err_pid*
131 |
132 | ### AndroidStudio ###
133 | # Covers files to be ignored for android development using Android Studio.
134 |
135 | # Built application files
136 |
137 | # Files for the ART/Dalvik VM
138 |
139 | # Java class files
140 |
141 | # Generated files
142 |
143 | # Gradle files
144 | .gradle
145 |
146 | # Signing files
147 | .signing/
148 |
149 | # Local configuration file (sdk path, etc)
150 |
151 | # Proguard folder generated by Eclipse
152 |
153 | # Log Files
154 |
155 | # Android Studio
156 | /*/build/
157 | /*/local.properties
158 | /*/out
159 | /*/*/build
160 | /*/*/production
161 | *.ipr
162 | *~
163 | *.swp
164 |
165 | # Android Patch
166 |
167 | # External native build folder generated in Android Studio 2.2 and later
168 |
169 | # NDK
170 | obj/
171 |
172 | # IntelliJ IDEA
173 | *.iws
174 | /out/
175 |
176 | # User-specific configurations
177 | .idea/caches/
178 | .idea/libraries/
179 | .idea/shelf/
180 | .idea/.name
181 | .idea/compiler.xml
182 | .idea/copyright/profiles_settings.xml
183 | .idea/encodings.xml
184 | .idea/misc.xml
185 | .idea/scopes/scope_settings.xml
186 | .idea/vcs.xml
187 | .idea/jsLibraryMappings.xml
188 | .idea/datasources.xml
189 | .idea/dataSources.ids
190 | .idea/sqlDataSources.xml
191 | .idea/dynamic.xml
192 | .idea/uiDesigner.xml
193 |
194 | # OS-specific files
195 | .DS_Store
196 | .DS_Store?
197 | ._*
198 | .Spotlight-V100
199 | .Trashes
200 | ehthumbs.db
201 | Thumbs.db
202 |
203 | # Legacy Eclipse project files
204 | .classpath
205 | .project
206 | .cproject
207 | .settings/
208 |
209 | # Mobile Tools for Java (J2ME)
210 |
211 | # Package Files #
212 |
213 | # virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml)
214 |
215 | ## Plugin-specific files:
216 |
217 | # mpeltonen/sbt-idea plugin
218 | .idea_modules/
219 |
220 | # JIRA plugin
221 | atlassian-ide-plugin.xml
222 |
223 | # Mongo Explorer plugin
224 | .idea/mongoSettings.xml
225 |
226 | # Crashlytics plugin (for Android Studio and IntelliJ)
227 | com_crashlytics_export_strings.xml
228 | crashlytics.properties
229 | crashlytics-build.properties
230 | fabric.properties
231 |
232 | ### AndroidStudio Patch ###
233 |
234 | !/gradle/wrapper/gradle-wrapper.jar
235 |
236 | # End of https://www.gitignore.io/api/kotlin,android,androidstudio
--------------------------------------------------------------------------------
/app/src/main/java/com/github/rahul_gill/attendance/util/Preference.kt:
--------------------------------------------------------------------------------
1 | package com.github.rahul_gill.attendance.util
2 |
3 | import android.content.Context
4 | import androidx.compose.runtime.Composable
5 | import androidx.datastore.core.DataStore
6 | import androidx.datastore.preferences.core.Preferences
7 | import androidx.datastore.preferences.core.booleanPreferencesKey
8 | import androidx.datastore.preferences.core.edit
9 | import androidx.datastore.preferences.core.intPreferencesKey
10 | import androidx.datastore.preferences.core.longPreferencesKey
11 | import androidx.datastore.preferences.core.stringPreferencesKey
12 | import androidx.datastore.preferences.preferencesDataStore
13 | import androidx.lifecycle.compose.collectAsStateWithLifecycle
14 | import kotlinx.coroutines.flow.Flow
15 | import kotlinx.coroutines.flow.firstOrNull
16 | import kotlinx.coroutines.flow.map
17 | import kotlinx.coroutines.runBlocking
18 |
19 |
20 | private val Context.dataStore: DataStore by preferencesDataStore(name = "settings")
21 |
22 | interface Preference {
23 | fun setValue(value: T)
24 | val key: String
25 | val defaultValue: T
26 | val observableValue: Flow
27 | val value: T
28 | get() = runBlocking {
29 | observableValue.firstOrNull() ?: defaultValue
30 | }
31 |
32 | @Composable
33 | fun asState() = observableValue.collectAsStateWithLifecycle(initialValue = value)
34 | }
35 |
36 | /**
37 | * Handle default values carefully, just like in enumPreference
38 | */
39 | fun customPreference(
40 | backingPref: Preference,
41 | defaultValue: T,
42 | serialize: (T) -> BackingT,
43 | deserialize: (BackingT) -> T
44 | ) = object : Preference {
45 |
46 | override val key = backingPref.key
47 | override val defaultValue = defaultValue
48 | override val observableValue: Flow
49 | get() = backingPref.observableValue.map(deserialize)
50 |
51 | override fun setValue(value: T) {
52 | backingPref.setValue(serialize(value))
53 | }
54 | }
55 |
56 | inline fun > enumPreference(
57 | key: String,
58 | defaultValue: T
59 | ) = customPreference(
60 | backingPref = IntPreference( key, Int.MAX_VALUE),
61 | defaultValue,
62 | serialize = { it.ordinal },
63 | deserialize = {
64 | if (it == Int.MAX_VALUE) {
65 | defaultValue
66 | } else {
67 | enumValues().getOrNull(it) ?: defaultValue
68 |
69 | }
70 | }
71 | )
72 |
73 | class IntPreference(
74 |
75 | override val key: String,
76 | override val defaultValue: Int
77 | ) : Preference {
78 |
79 | private val backingKey = intPreferencesKey(key)
80 | override fun setValue(value: Int) {
81 | runBlocking {
82 | applicationContextGlobal.dataStore.edit { prefs ->
83 | prefs[backingKey] = value
84 | }
85 | }
86 | }
87 |
88 | override val observableValue: Flow
89 | get() = applicationContextGlobal.dataStore.data.map { pref ->
90 | pref[backingKey] ?: defaultValue
91 | }
92 | }
93 |
94 | class BooleanPreference(
95 |
96 | override val key: String,
97 | override val defaultValue: Boolean
98 | ) : Preference {
99 |
100 | private val backingKey = booleanPreferencesKey(key)
101 | override fun setValue(value: Boolean) {
102 | runBlocking {
103 | applicationContextGlobal.dataStore.edit { prefs ->
104 | prefs[backingKey] = value
105 | }
106 | }
107 | }
108 |
109 | override val observableValue: Flow
110 | get() = applicationContextGlobal.dataStore.data.map { pref ->
111 | pref[backingKey] ?: defaultValue
112 | }
113 | }
114 |
115 |
116 | class LongPreference(
117 |
118 | override val key: String,
119 | override val defaultValue: Long
120 | ) : Preference {
121 |
122 | private val backingKey = longPreferencesKey(key)
123 | override fun setValue(value: Long) {
124 | runBlocking {
125 | applicationContextGlobal.dataStore.edit { prefs ->
126 | prefs[backingKey] = value
127 | }
128 | }
129 | }
130 |
131 | override val observableValue: Flow
132 | get() = applicationContextGlobal.dataStore.data.map { pref ->
133 | pref[backingKey] ?: defaultValue
134 | }
135 | }
136 |
137 | class StringPreference(
138 |
139 | override val key: String,
140 | override val defaultValue: String
141 | ) : Preference {
142 |
143 | private val backingKey = stringPreferencesKey(key)
144 | override fun setValue(value: String) {
145 | runBlocking {
146 | applicationContextGlobal.dataStore.edit { prefs ->
147 | prefs[backingKey] = value
148 | }
149 | }
150 | }
151 |
152 | override val observableValue: Flow
153 | get() = applicationContextGlobal.dataStore.data.map { pref ->
154 | pref[backingKey] ?: defaultValue
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ]; do
14 | ls=$(ls -ld "$PRG")
15 | link=$(expr "$ls" : '.*-> \(.*\)$')
16 | if expr "$link" : '/.*' >/dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=$(dirname "$PRG")"/$link"
20 | fi
21 | done
22 | SAVED="$(pwd)"
23 | cd "$(dirname \"$PRG\")/" >/dev/null
24 | APP_HOME="$(pwd -P)"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=$(basename "$0")
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn() {
37 | echo "$*"
38 | }
39 |
40 | die() {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "$(uname)" in
53 | CYGWIN*)
54 | cygwin=true
55 | ;;
56 | Darwin*)
57 | darwin=true
58 | ;;
59 | MINGW*)
60 | msys=true
61 | ;;
62 | NONSTOP*)
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ]; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ]; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ]; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ]; then
93 | MAX_FD_LIMIT=$(ulimit -H -n)
94 | if [ $? -eq 0 ]; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ]; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ]; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin; then
114 | APP_HOME=$(cygpath --path --mixed "$APP_HOME")
115 | CLASSPATH=$(cygpath --path --mixed "$CLASSPATH")
116 | JAVACMD=$(cygpath --unix "$JAVACMD")
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=$(find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null)
120 | SEP=""
121 | for dir in $ROOTDIRSRAW; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ]; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@"; do
133 | CHECK=$(echo "$arg" | egrep -c "$OURCYGPATTERN" -)
134 | CHECK2=$(echo "$arg" | egrep -c "^-") ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ]; then ### Added a condition
137 | eval $(echo args$i)=$(cygpath --path --ignore --mixed "$arg")
138 | else
139 | eval $(echo args$i)="\"$arg\""
140 | fi
141 | i=$((i + 1))
142 | done
143 | case $i in
144 | 0) set -- ;;
145 | 1) set -- "$args0" ;;
146 | 2) set -- "$args0" "$args1" ;;
147 | 3) set -- "$args0" "$args1" "$args2" ;;
148 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save() {
159 | for i; do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/"; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/rahul_gill/attendance/ui/comps/SetClassStatusSheet.kt:
--------------------------------------------------------------------------------
1 | package com.github.rahul_gill.attendance.ui.comps
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.PaddingValues
5 | import androidx.compose.foundation.layout.Row
6 | import androidx.compose.foundation.layout.Spacer
7 | import androidx.compose.foundation.layout.fillMaxWidth
8 | import androidx.compose.foundation.layout.height
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.foundation.selection.selectable
11 | import androidx.compose.foundation.selection.selectableGroup
12 | import androidx.compose.material3.ButtonDefaults
13 | import androidx.compose.material3.MaterialTheme
14 | import androidx.compose.material3.RadioButton
15 | import androidx.compose.material3.Text
16 | import androidx.compose.material3.TextButton
17 | import androidx.compose.runtime.Composable
18 | import androidx.compose.runtime.getValue
19 | import androidx.compose.runtime.mutableStateOf
20 | import androidx.compose.runtime.remember
21 | import androidx.compose.runtime.setValue
22 | import androidx.compose.ui.Alignment
23 | import androidx.compose.ui.Modifier
24 | import androidx.compose.ui.res.stringResource
25 | import androidx.compose.ui.semantics.Role
26 | import androidx.compose.ui.unit.dp
27 | import com.github.rahul_gill.attendance.R
28 | import com.github.rahul_gill.attendance.db.AttendanceRecordHybrid
29 | import com.github.rahul_gill.attendance.db.CourseClassStatus
30 | import com.github.rahul_gill.attendance.util.dateFormatter
31 | import com.github.rahul_gill.attendance.util.timeFormatter
32 |
33 | @Composable
34 | fun SetClassStatusSheet(
35 | todayCourseItem: AttendanceRecordHybrid,
36 | onDismissRequest: () -> Unit,
37 | setClasStatus: (CourseClassStatus) -> Unit,
38 | onDeleteItem: (() -> Unit)? = null
39 | ) {
40 | var newStatus by remember {
41 | mutableStateOf(todayCourseItem.classStatus)
42 | }
43 | BaseDialog(
44 | onDismissRequest = onDismissRequest,
45 | dialogPadding = PaddingValues(0.dp)
46 | ) {
47 | Text(
48 | text = stringResource(
49 | id = R.string.attendance_status_setter_info,
50 | todayCourseItem.courseName,
51 | todayCourseItem.startTime.format(timeFormatter),
52 | todayCourseItem.endTime.format(timeFormatter),
53 | if (todayCourseItem is AttendanceRecordHybrid.ExtraClass)
54 | stringResource(R.string.attendance_status_setter_info_extra_class)
55 | else "",
56 | stringResource(
57 | R.string.attendance_status_setter_info_on_date,
58 | todayCourseItem.date.format(dateFormatter)
59 | )
60 | ),
61 | style = MaterialTheme.typography.titleLarge,
62 | modifier = Modifier.padding(16.dp)
63 | )
64 | Spacer(modifier = Modifier.height(16.dp))
65 | ClassStatusOptions(newStatus) { newStatus = it }
66 | Row(modifier = Modifier.fillMaxWidth()) {
67 | if (onDeleteItem != null) {
68 | TextButton(
69 | colors = ButtonDefaults.textButtonColors(
70 | contentColor = MaterialTheme.colorScheme.error
71 | ),
72 | onClick = {
73 | onDeleteItem()
74 | onDismissRequest()
75 | }
76 | ) {
77 | Text(text = stringResource(id = R.string.delete_record))
78 | }
79 | }
80 | Spacer(modifier = Modifier.weight(1f))
81 | TextButton(onClick = onDismissRequest) {
82 | Text(text = stringResource(id = R.string.cancel))
83 | }
84 | TextButton(onClick = {
85 | setClasStatus(newStatus)
86 | onDismissRequest()
87 | }) {
88 | Text(text = stringResource(id = R.string.ok))
89 | }
90 | }
91 | }
92 | }
93 |
94 | @Composable
95 | fun ClassStatusOptions(
96 | initialStatus: CourseClassStatus,
97 | setClassStatus: (CourseClassStatus) -> Unit
98 | ) {
99 | Column(Modifier.selectableGroup()) {
100 | CourseClassStatus.entries.forEach { dayOfWeek ->
101 | Row(
102 | Modifier
103 | .fillMaxWidth()
104 | .height(56.dp)
105 | .selectable(
106 | selected = (dayOfWeek == initialStatus),
107 | onClick = { setClassStatus(dayOfWeek) },
108 | role = Role.RadioButton
109 | )
110 | .padding(horizontal = 16.dp),
111 | verticalAlignment = Alignment.CenterVertically
112 | ) {
113 | RadioButton(
114 | selected = (dayOfWeek == initialStatus),
115 | onClick = null // null recommended for accessibility with screenreaders
116 | )
117 | Text(
118 | text = stringResource(
119 | id = when (dayOfWeek) {
120 | CourseClassStatus.Present -> R.string.present
121 | CourseClassStatus.Absent -> R.string.absent
122 | CourseClassStatus.Cancelled -> R.string.cancelled
123 | CourseClassStatus.Unset -> R.string.not_set
124 | }
125 | ),
126 | style = MaterialTheme.typography.bodyLarge,
127 | modifier = Modifier.padding(start = 16.dp)
128 | )
129 | }
130 | }
131 | }
132 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/rahul_gill/attendance/ui/screens/CourseEditScreen.kt:
--------------------------------------------------------------------------------
1 | package com.github.rahul_gill.attendance.ui.screens
2 |
3 | import android.widget.Toast
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.Spacer
6 | import androidx.compose.foundation.layout.fillMaxWidth
7 | import androidx.compose.foundation.layout.height
8 | import androidx.compose.foundation.layout.imePadding
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.material.icons.Icons
11 | import androidx.compose.material.icons.automirrored.filled.ArrowBack
12 | import androidx.compose.material.icons.filled.Clear
13 | import androidx.compose.material3.ExperimentalMaterial3Api
14 | import androidx.compose.material3.ExtendedFloatingActionButton
15 | import androidx.compose.material3.Icon
16 | import androidx.compose.material3.IconButton
17 | import androidx.compose.material3.LargeTopAppBar
18 | import androidx.compose.material3.OutlinedTextField
19 | import androidx.compose.material3.Scaffold
20 | import androidx.compose.material3.Slider
21 | import androidx.compose.material3.SnackbarHostState
22 | import androidx.compose.material3.Text
23 | import androidx.compose.material3.TopAppBarDefaults
24 | import androidx.compose.material3.rememberTopAppBarState
25 | import androidx.compose.runtime.Composable
26 | import androidx.compose.runtime.getValue
27 | import androidx.compose.runtime.mutableIntStateOf
28 | import androidx.compose.runtime.mutableStateOf
29 | import androidx.compose.runtime.remember
30 | import androidx.compose.runtime.rememberCoroutineScope
31 | import androidx.compose.runtime.saveable.rememberSaveable
32 | import androidx.compose.runtime.setValue
33 | import androidx.compose.ui.Modifier
34 | import androidx.compose.ui.input.nestedscroll.nestedScroll
35 | import androidx.compose.ui.platform.LocalContext
36 | import androidx.compose.ui.res.painterResource
37 | import androidx.compose.ui.res.stringResource
38 | import androidx.compose.ui.unit.dp
39 | import com.github.rahul_gill.attendance.R
40 | import com.github.rahul_gill.attendance.db.CourseDetailsOverallItem
41 | import kotlinx.coroutines.launch
42 |
43 | @OptIn(ExperimentalMaterial3Api::class)
44 | @Composable
45 | fun CourseEditScreen(
46 | courseDetails: CourseDetailsOverallItem,
47 | onGoBack: () -> Unit,
48 | onSave: (courseName: String, requiredPercentage: Int) -> Unit
49 | ) {
50 | val scope = rememberCoroutineScope()
51 | val snackbarHostState = remember { SnackbarHostState() }
52 | val context = LocalContext.current
53 |
54 |
55 | var newCourseName by rememberSaveable {
56 | mutableStateOf(courseDetails.courseName)
57 | }
58 | var newRequiredAttendancePercentage by rememberSaveable {
59 | mutableIntStateOf(courseDetails.requiredAttendance.toInt())
60 | }
61 |
62 | val scrollBehavior =
63 | TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())
64 | Scaffold(
65 | modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
66 | topBar = {
67 | LargeTopAppBar(title = {
68 | Text(
69 | text = stringResource(
70 | id = R.string.edit_course_screen_title,
71 | courseDetails.courseName
72 | ),
73 | )
74 | }, navigationIcon = {
75 | IconButton(onClick = onGoBack) {
76 | Icon(
77 | imageVector = Icons.AutoMirrored.Filled.ArrowBack,
78 | contentDescription = stringResource(id = R.string.go_back_screen)
79 | )
80 | }
81 | }, scrollBehavior = scrollBehavior
82 | )
83 | },
84 | floatingActionButton = {
85 | ExtendedFloatingActionButton(
86 | onClick = onClickSave@{
87 | if (newCourseName.isBlank()) {
88 | scope.launch {
89 | snackbarHostState.showSnackbar(
90 | message = context.getString(R.string.error_course_name_blank),
91 | withDismissAction = true
92 | )
93 | }
94 | return@onClickSave
95 | }
96 | onSave(newCourseName, newRequiredAttendancePercentage)
97 | Toast.makeText(context, R.string.course_updated, Toast.LENGTH_SHORT).show()
98 | onGoBack()
99 | },
100 | modifier = Modifier.imePadding()
101 | ) {
102 | Icon(
103 | painter = painterResource(id = R.drawable.baseline_save_24),
104 | contentDescription = null
105 | )
106 | Text(text = stringResource(id = R.string.save))
107 | }
108 | }
109 | ) { innerPadding ->
110 | Column(
111 | modifier = Modifier
112 | .padding(innerPadding)
113 | .padding(horizontal = 16.dp)
114 | ) {
115 | Spacer(modifier = Modifier.height(16.dp))
116 | OutlinedTextField(
117 | value = newCourseName,
118 | onValueChange = { newCourseName = it },
119 | maxLines = 1,
120 | trailingIcon = {
121 | if (newCourseName.isNotBlank()) {
122 | IconButton(onClick = { newCourseName = "" }) {
123 | Icon(
124 | imageVector = Icons.Default.Clear,
125 | contentDescription = stringResource(id = R.string.clear_text)
126 | )
127 | }
128 | }
129 | },
130 | modifier = Modifier.fillMaxWidth(),
131 | label = {
132 | Text(text = stringResource(id = R.string.course_name))
133 | }
134 | )
135 | Spacer(modifier = Modifier.height(16.dp))
136 | Text(
137 | text = stringResource(
138 | id = R.string.required_attendance_text,
139 | newRequiredAttendancePercentage
140 | )
141 | )
142 | Spacer(modifier = Modifier.height(8.dp))
143 | Slider(
144 | value = newRequiredAttendancePercentage.toFloat(),
145 | onValueChange = { newRequiredAttendancePercentage = it.toInt() },
146 | steps = 100,
147 | valueRange = 1f..100f
148 | )
149 | Spacer(modifier = Modifier.height(16.dp))
150 |
151 | }
152 | }
153 | }
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | CFPropertyList (3.0.7)
5 | base64
6 | nkf
7 | rexml
8 | addressable (2.8.6)
9 | public_suffix (>= 2.0.2, < 6.0)
10 | artifactory (3.0.17)
11 | atomos (0.1.3)
12 | aws-eventstream (1.3.0)
13 | aws-partitions (1.924.0)
14 | aws-sdk-core (3.194.1)
15 | aws-eventstream (~> 1, >= 1.3.0)
16 | aws-partitions (~> 1, >= 1.651.0)
17 | aws-sigv4 (~> 1.8)
18 | jmespath (~> 1, >= 1.6.1)
19 | aws-sdk-kms (1.80.0)
20 | aws-sdk-core (~> 3, >= 3.193.0)
21 | aws-sigv4 (~> 1.1)
22 | aws-sdk-s3 (1.149.0)
23 | aws-sdk-core (~> 3, >= 3.194.0)
24 | aws-sdk-kms (~> 1)
25 | aws-sigv4 (~> 1.8)
26 | aws-sigv4 (1.8.0)
27 | aws-eventstream (~> 1, >= 1.0.2)
28 | babosa (1.0.4)
29 | base64 (0.2.0)
30 | claide (1.1.0)
31 | colored (1.2)
32 | colored2 (3.1.2)
33 | commander (4.6.0)
34 | highline (~> 2.0.0)
35 | declarative (0.0.20)
36 | digest-crc (0.6.5)
37 | rake (>= 12.0.0, < 14.0.0)
38 | domain_name (0.6.20240107)
39 | dotenv (2.8.1)
40 | emoji_regex (3.2.3)
41 | excon (0.110.0)
42 | faraday (1.10.3)
43 | faraday-em_http (~> 1.0)
44 | faraday-em_synchrony (~> 1.0)
45 | faraday-excon (~> 1.1)
46 | faraday-httpclient (~> 1.0)
47 | faraday-multipart (~> 1.0)
48 | faraday-net_http (~> 1.0)
49 | faraday-net_http_persistent (~> 1.0)
50 | faraday-patron (~> 1.0)
51 | faraday-rack (~> 1.0)
52 | faraday-retry (~> 1.0)
53 | ruby2_keywords (>= 0.0.4)
54 | faraday-cookie_jar (0.0.7)
55 | faraday (>= 0.8.0)
56 | http-cookie (~> 1.0.0)
57 | faraday-em_http (1.0.0)
58 | faraday-em_synchrony (1.0.0)
59 | faraday-excon (1.1.0)
60 | faraday-httpclient (1.0.1)
61 | faraday-multipart (1.0.4)
62 | multipart-post (~> 2)
63 | faraday-net_http (1.0.1)
64 | faraday-net_http_persistent (1.2.0)
65 | faraday-patron (1.0.0)
66 | faraday-rack (1.0.0)
67 | faraday-retry (1.0.3)
68 | faraday_middleware (1.2.0)
69 | faraday (~> 1.0)
70 | fastimage (2.3.1)
71 | fastlane (2.220.0)
72 | CFPropertyList (>= 2.3, < 4.0.0)
73 | addressable (>= 2.8, < 3.0.0)
74 | artifactory (~> 3.0)
75 | aws-sdk-s3 (~> 1.0)
76 | babosa (>= 1.0.3, < 2.0.0)
77 | bundler (>= 1.12.0, < 3.0.0)
78 | colored (~> 1.2)
79 | commander (~> 4.6)
80 | dotenv (>= 2.1.1, < 3.0.0)
81 | emoji_regex (>= 0.1, < 4.0)
82 | excon (>= 0.71.0, < 1.0.0)
83 | faraday (~> 1.0)
84 | faraday-cookie_jar (~> 0.0.6)
85 | faraday_middleware (~> 1.0)
86 | fastimage (>= 2.1.0, < 3.0.0)
87 | gh_inspector (>= 1.1.2, < 2.0.0)
88 | google-apis-androidpublisher_v3 (~> 0.3)
89 | google-apis-playcustomapp_v1 (~> 0.1)
90 | google-cloud-env (>= 1.6.0, < 2.0.0)
91 | google-cloud-storage (~> 1.31)
92 | highline (~> 2.0)
93 | http-cookie (~> 1.0.5)
94 | json (< 3.0.0)
95 | jwt (>= 2.1.0, < 3)
96 | mini_magick (>= 4.9.4, < 5.0.0)
97 | multipart-post (>= 2.0.0, < 3.0.0)
98 | naturally (~> 2.2)
99 | optparse (>= 0.1.1, < 1.0.0)
100 | plist (>= 3.1.0, < 4.0.0)
101 | rubyzip (>= 2.0.0, < 3.0.0)
102 | security (= 0.1.5)
103 | simctl (~> 1.6.3)
104 | terminal-notifier (>= 2.0.0, < 3.0.0)
105 | terminal-table (~> 3)
106 | tty-screen (>= 0.6.3, < 1.0.0)
107 | tty-spinner (>= 0.8.0, < 1.0.0)
108 | word_wrap (~> 1.0.0)
109 | xcodeproj (>= 1.13.0, < 2.0.0)
110 | xcpretty (~> 0.3.0)
111 | xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
112 | gh_inspector (1.1.3)
113 | google-apis-androidpublisher_v3 (0.54.0)
114 | google-apis-core (>= 0.11.0, < 2.a)
115 | google-apis-core (0.11.3)
116 | addressable (~> 2.5, >= 2.5.1)
117 | googleauth (>= 0.16.2, < 2.a)
118 | httpclient (>= 2.8.1, < 3.a)
119 | mini_mime (~> 1.0)
120 | representable (~> 3.0)
121 | retriable (>= 2.0, < 4.a)
122 | rexml
123 | google-apis-iamcredentials_v1 (0.17.0)
124 | google-apis-core (>= 0.11.0, < 2.a)
125 | google-apis-playcustomapp_v1 (0.13.0)
126 | google-apis-core (>= 0.11.0, < 2.a)
127 | google-apis-storage_v1 (0.31.0)
128 | google-apis-core (>= 0.11.0, < 2.a)
129 | google-cloud-core (1.7.0)
130 | google-cloud-env (>= 1.0, < 3.a)
131 | google-cloud-errors (~> 1.0)
132 | google-cloud-env (1.6.0)
133 | faraday (>= 0.17.3, < 3.0)
134 | google-cloud-errors (1.4.0)
135 | google-cloud-storage (1.47.0)
136 | addressable (~> 2.8)
137 | digest-crc (~> 0.4)
138 | google-apis-iamcredentials_v1 (~> 0.1)
139 | google-apis-storage_v1 (~> 0.31.0)
140 | google-cloud-core (~> 1.6)
141 | googleauth (>= 0.16.2, < 2.a)
142 | mini_mime (~> 1.0)
143 | googleauth (1.8.1)
144 | faraday (>= 0.17.3, < 3.a)
145 | jwt (>= 1.4, < 3.0)
146 | multi_json (~> 1.11)
147 | os (>= 0.9, < 2.0)
148 | signet (>= 0.16, < 2.a)
149 | highline (2.0.3)
150 | http-cookie (1.0.5)
151 | domain_name (~> 0.5)
152 | httpclient (2.8.3)
153 | jmespath (1.6.2)
154 | json (2.7.2)
155 | jwt (2.8.1)
156 | base64
157 | mini_magick (4.12.0)
158 | mini_mime (1.1.5)
159 | multi_json (1.15.0)
160 | multipart-post (2.4.0)
161 | nanaimo (0.3.0)
162 | naturally (2.2.1)
163 | nkf (0.2.0)
164 | optparse (0.5.0)
165 | os (1.1.4)
166 | plist (3.7.1)
167 | public_suffix (5.0.5)
168 | rake (13.2.1)
169 | representable (3.2.0)
170 | declarative (< 0.1.0)
171 | trailblazer-option (>= 0.1.1, < 0.2.0)
172 | uber (< 0.2.0)
173 | retriable (3.1.2)
174 | rexml (3.2.6)
175 | rouge (2.0.7)
176 | ruby2_keywords (0.0.5)
177 | rubyzip (2.3.2)
178 | security (0.1.5)
179 | signet (0.19.0)
180 | addressable (~> 2.8)
181 | faraday (>= 0.17.5, < 3.a)
182 | jwt (>= 1.5, < 3.0)
183 | multi_json (~> 1.10)
184 | simctl (1.6.10)
185 | CFPropertyList
186 | naturally
187 | terminal-notifier (2.0.0)
188 | terminal-table (3.0.2)
189 | unicode-display_width (>= 1.1.1, < 3)
190 | trailblazer-option (0.1.2)
191 | tty-cursor (0.7.1)
192 | tty-screen (0.8.2)
193 | tty-spinner (0.9.3)
194 | tty-cursor (~> 0.7)
195 | uber (0.1.0)
196 | unicode-display_width (2.5.0)
197 | word_wrap (1.0.0)
198 | xcodeproj (1.24.0)
199 | CFPropertyList (>= 2.3.3, < 4.0)
200 | atomos (~> 0.1.3)
201 | claide (>= 1.0.2, < 2.0)
202 | colored2 (~> 3.1)
203 | nanaimo (~> 0.3.0)
204 | rexml (~> 3.2.4)
205 | xcpretty (0.3.0)
206 | rouge (~> 2.0.7)
207 | xcpretty-travis-formatter (1.0.1)
208 | xcpretty (~> 0.2, >= 0.0.7)
209 |
210 | PLATFORMS
211 | ruby
212 | x86_64-linux
213 |
214 | DEPENDENCIES
215 | fastlane
216 |
217 | BUNDLED WITH
218 | 2.5.10
219 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/rahul_gill/attendance/ui/comps/Dialog.kt:
--------------------------------------------------------------------------------
1 | package com.github.rahul_gill.attendance.ui.comps
2 |
3 |
4 | import android.view.Gravity
5 | import androidx.compose.foundation.background
6 | import androidx.compose.foundation.layout.Arrangement
7 | import androidx.compose.foundation.layout.Box
8 | import androidx.compose.foundation.layout.Column
9 | import androidx.compose.foundation.layout.ColumnScope
10 | import androidx.compose.foundation.layout.PaddingValues
11 | import androidx.compose.foundation.layout.Row
12 | import androidx.compose.foundation.layout.Spacer
13 | import androidx.compose.foundation.layout.fillMaxSize
14 | import androidx.compose.foundation.layout.fillMaxWidth
15 | import androidx.compose.foundation.layout.height
16 | import androidx.compose.foundation.layout.padding
17 | import androidx.compose.foundation.layout.widthIn
18 | import androidx.compose.foundation.shape.RoundedCornerShape
19 | import androidx.compose.material3.Button
20 | import androidx.compose.material3.LocalContentColor
21 | import androidx.compose.material3.MaterialTheme
22 | import androidx.compose.material3.ProvideTextStyle
23 | import androidx.compose.material3.Scaffold
24 | import androidx.compose.material3.Text
25 | import androidx.compose.runtime.Composable
26 | import androidx.compose.runtime.CompositionLocalProvider
27 | import androidx.compose.runtime.getValue
28 | import androidx.compose.runtime.mutableStateOf
29 | import androidx.compose.runtime.remember
30 | import androidx.compose.runtime.setValue
31 | import androidx.compose.ui.Alignment
32 | import androidx.compose.ui.Modifier
33 | import androidx.compose.ui.draw.clip
34 | import androidx.compose.ui.draw.shadow
35 | import androidx.compose.ui.graphics.Color
36 | import androidx.compose.ui.platform.LocalView
37 | import androidx.compose.ui.semantics.paneTitle
38 | import androidx.compose.ui.semantics.semantics
39 | //import androidx.compose.ui.tooling.preview.Preview
40 | import androidx.compose.ui.unit.Dp
41 | import androidx.compose.ui.unit.dp
42 | import androidx.compose.ui.window.Dialog
43 | import androidx.compose.ui.window.DialogProperties
44 | import androidx.compose.ui.window.DialogWindowProvider
45 |
46 |
47 | @Composable
48 | fun BaseDialog(
49 | modifier: Modifier = Modifier,
50 | backgroundColor: Color = MaterialTheme.colorScheme.surfaceVariant,
51 | properties: DialogProperties = DialogProperties(
52 | dismissOnClickOutside = true,
53 | usePlatformDefaultWidth = false
54 | ),
55 | onDismissRequest: () -> Unit,
56 | dialogPadding: PaddingValues = BaseDialogDefaults.dialogMargins,
57 | contentPadding: PaddingValues = BaseDialogDefaults.contentPadding,
58 | minWidth: Dp = 280.dp,
59 | content: @Composable ColumnScope.() -> Unit
60 | ) {
61 | Dialog(
62 | onDismissRequest = onDismissRequest,
63 | properties = properties,
64 | ) {
65 | (LocalView.current.parent as? DialogWindowProvider)?.window?.run {
66 | setDimAmount(BaseDialogDefaults.dimAmount)
67 | setGravity(Gravity.BOTTOM)
68 | }
69 | Box(
70 | modifier = modifier
71 | .widthIn(min = minWidth)
72 | .padding(dialogPadding)
73 | .semantics { paneTitle = "Dialog" }
74 | ) {
75 | Column(
76 | modifier = Modifier
77 | .clip(BaseDialogDefaults.shape)
78 | .shadow(elevation = BaseDialogDefaults.elevation)
79 | .background(
80 | color = backgroundColor,
81 | shape = BaseDialogDefaults.shape
82 | )
83 | .padding(contentPadding),
84 | ) {
85 | CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant) {
86 | content()
87 | }
88 | }
89 | }
90 | }
91 | }
92 |
93 | //@Preview(showSystemUi = true, showBackground = true)
94 | @Composable
95 | private fun AlertDialogPreview() {
96 | var show by remember {
97 | mutableStateOf(true)
98 | }
99 | MaterialTheme {
100 | Scaffold(Modifier.fillMaxSize()) {
101 | Button(onClick = { show = true }, modifier = Modifier.padding(it)) {
102 |
103 | Text(text = "Some Text")
104 | }
105 | }
106 | }
107 | if (show) {
108 | AlertDialog(
109 | onDismissRequest = { show = false },
110 | title = { Text(text = "Alert Dialog Title") },
111 | body = {
112 | Text(text = "Some text ", modifier = Modifier.padding(8.dp))
113 |
114 | },
115 | buttonBar = {
116 | Button(onClick = { show = false }) {
117 | Text(text = "Cancel")
118 | }
119 |
120 | Button(onClick = { show = false }) {
121 | Text(text = "OK")
122 | }
123 | }
124 | )
125 | }
126 | }
127 |
128 | @Composable
129 | fun AlertDialog(
130 | modifier: Modifier = Modifier,
131 | onDismissRequest: () -> Unit,
132 | title: (@Composable () -> Unit)? = null,
133 | body: (@Composable () -> Unit)? = null,
134 | buttonBar: (@Composable () -> Unit)? = null
135 | ) {
136 | BaseDialog(
137 | modifier = modifier,
138 | onDismissRequest = onDismissRequest
139 | ) {
140 | Column(
141 | verticalArrangement = Arrangement.Top,
142 | horizontalAlignment = Alignment.Start
143 | ) {
144 | ProvideTextStyle(MaterialTheme.typography.titleLarge) {
145 | title?.let { it() }
146 | }
147 | if (title != null && body != null) {
148 | Spacer(
149 | modifier = Modifier
150 | .height(8.dp)
151 | )
152 | }
153 | ProvideTextStyle(MaterialTheme.typography.bodyLarge) {
154 | body?.let { it() }
155 | }
156 | if (buttonBar != null) {
157 | Spacer(
158 | modifier = Modifier
159 | .height(26.dp)
160 | )
161 | }
162 | Row(horizontalArrangement = Arrangement.End, modifier = Modifier.fillMaxWidth()) {
163 | buttonBar?.let { it() }
164 | }
165 | }
166 | }
167 | }
168 |
169 | object BaseDialogDefaults {
170 |
171 | val contentPadding = PaddingValues(
172 | all = 24.dp
173 | )
174 |
175 | val contentPaddingAlternative = PaddingValues(
176 | vertical = 24.dp, horizontal = 8.dp
177 | )
178 |
179 | val dialogMargins = PaddingValues(
180 | bottom = 12.dp,
181 | start = 12.dp,
182 | end = 12.dp
183 | )
184 |
185 | val shape = RoundedCornerShape(
186 | size = 26.dp
187 | )
188 |
189 | const val dimAmount = 0.65F
190 |
191 | val elevation = 1.dp
192 |
193 | const val animDuration = 150
194 |
195 | }
--------------------------------------------------------------------------------
/fastlane/metadata/android/screenshots.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | fastlane/screengrab
5 |
6 |
66 |
67 |
68 | en-US
69 |
70 |
104 |
105 |
![]()
106 |
107 |
108 |
209 |
210 |
211 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/rahul_gill/attendance/ui/comps/Tabs.kt:
--------------------------------------------------------------------------------
1 | package com.github.rahul_gill.attendance.ui.comps
2 |
3 |
4 | import androidx.compose.foundation.Canvas
5 | import androidx.compose.foundation.clickable
6 | import androidx.compose.foundation.interaction.MutableInteractionSource
7 | import androidx.compose.foundation.layout.Arrangement
8 | import androidx.compose.foundation.layout.Box
9 | import androidx.compose.foundation.layout.Column
10 | import androidx.compose.foundation.layout.PaddingValues
11 | import androidx.compose.foundation.layout.Row
12 | import androidx.compose.foundation.layout.RowScope
13 | import androidx.compose.foundation.layout.fillMaxSize
14 | import androidx.compose.foundation.layout.fillMaxWidth
15 | import androidx.compose.foundation.layout.height
16 | import androidx.compose.foundation.layout.padding
17 | import androidx.compose.foundation.layout.width
18 | import androidx.compose.foundation.shape.RoundedCornerShape
19 | import androidx.compose.material.ripple.rememberRipple
20 | import androidx.compose.material3.MaterialTheme
21 | import androidx.compose.material3.Text
22 | import androidx.compose.material3.minimumInteractiveComponentSize
23 | import androidx.compose.material3.ripple
24 | import androidx.compose.runtime.Composable
25 | import androidx.compose.runtime.getValue
26 | import androidx.compose.runtime.mutableIntStateOf
27 | import androidx.compose.runtime.remember
28 | import androidx.compose.runtime.setValue
29 | import androidx.compose.ui.Alignment
30 | import androidx.compose.ui.Modifier
31 | import androidx.compose.ui.draw.clip
32 | import androidx.compose.ui.geometry.Offset
33 | import androidx.compose.ui.graphics.Color
34 | import androidx.compose.ui.graphics.StrokeCap
35 | import androidx.compose.ui.layout.onGloballyPositioned
36 | import androidx.compose.ui.platform.LocalDensity
37 | import androidx.compose.ui.semantics.Role
38 | import androidx.compose.ui.text.font.FontWeight
39 | import androidx.compose.ui.unit.dp
40 |
41 |
42 | /**
43 | * Base composable for a group of multiple tabs, to be used as primary or secondary navigation utility.
44 | * Is actually only a wrapped [Row]
45 | *
46 | * @param modifier The [Modifier] to apply to the container
47 | * @param tabs The content, preferably multiple [TabItem]s or [CustomTabItem]s
48 | */
49 | @Composable
50 | fun Tabs(
51 | modifier: Modifier = Modifier,
52 | tabs: @Composable RowScope.() -> Unit
53 | ) {
54 | Row(
55 | modifier = Modifier
56 | .fillMaxWidth()
57 | .then(modifier),
58 | verticalAlignment = Alignment.CenterVertically,
59 | horizontalArrangement = Arrangement.SpaceEvenly
60 | ) {
61 | tabs()
62 | }
63 | }
64 |
65 |
66 |
67 | /**
68 | * Composable for a one-ui style [TabItem], to be used in a [Tabs] row.
69 | * Note: For proper usage, every [TabItem] should have a weight of 1, to be applied via [Modifier.weight()]
70 | *
71 | * @param modifier The [Modifier] to be applied to the container
72 | * @param colors The [TabsColors] to apply
73 | * @param onClick The callback invoked when the [TabItem] is clicked
74 | * @param text The text to be shown on the tab
75 | * @param selected Whether this tab is selected or not
76 | * @param interactionSource The [MutableInteractionSource]
77 | */
78 | @Composable
79 | fun TabItem(
80 | modifier: Modifier = Modifier,
81 | colors: TabsColors = tabsColors(),
82 | onClick: () -> Unit,
83 | enabled: Boolean = true,
84 | text: String,
85 | selected: Boolean,
86 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
87 | ) {
88 | Column(
89 | modifier = Modifier
90 | .clip(TabsDefaults.itemShape)
91 | .clickable(
92 | interactionSource = interactionSource,
93 | indication = ripple(
94 | color = colors.itemRipple
95 | ),
96 | role = Role.Tab,
97 | onClick = onClick,
98 | enabled = enabled
99 | )
100 | .minimumInteractiveComponentSize()
101 | .padding(TabsDefaults.itemPadding)
102 | .then(modifier),
103 | verticalArrangement = Arrangement
104 | .spacedBy(
105 | TabsDefaults.itemIndicatorSpacing,
106 | alignment = Alignment.CenterVertically
107 | ),
108 | horizontalAlignment = Alignment.CenterHorizontally
109 | ) {
110 | var textWidth by remember {
111 | mutableIntStateOf(0)
112 | }
113 |
114 | Text(
115 | modifier = Modifier //We need to measure the width of the text at runtime
116 | .onGloballyPositioned {
117 | textWidth = it.size.width
118 | },
119 | text = text,
120 | color = colors.itemIndicator,
121 | style = if(selected) MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Bold) else MaterialTheme.typography.bodyLarge
122 | )
123 |
124 |
125 | Box(
126 | modifier = Modifier
127 | .width(with(LocalDensity.current) { textWidth.toDp() })
128 | .height(TabsDefaults.itemIndicatorHeight)
129 | ) {
130 | Canvas(
131 | modifier = Modifier
132 | .fillMaxSize()
133 | ) {
134 | drawLine(
135 | color = if (selected) colors.itemIndicator else Color.Transparent,
136 | start = Offset(
137 | x = 0F,
138 | y = size.height / 2F
139 | ),
140 | end = Offset(
141 | x = size.width,
142 | y = size.height / 2F
143 | ),
144 | strokeWidth = TabsDefaults.itemIndicatorHeight.toPx(),
145 | cap = StrokeCap.Round
146 | )
147 | }
148 | }
149 | }
150 | }
151 | @Composable
152 | fun CustomTabItem(
153 | modifier: Modifier = Modifier,
154 | colors: TabsColors = tabsColors(),
155 | onClick: () -> Unit,
156 | enabled: Boolean = true,
157 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
158 | content: @Composable () -> Unit,
159 | ) {
160 | Box(
161 | modifier = modifier
162 | .height(TabsDefaults.customButtonHeight)
163 | .clip(TabsDefaults.itemShape)
164 | .clickable(
165 | interactionSource = interactionSource,
166 | indication = ripple(
167 | color = colors.itemRipple
168 | ),
169 | role = Role.Tab,
170 | onClick = onClick,
171 | enabled = enabled
172 | ),
173 | contentAlignment = Alignment.Center
174 | ) {
175 | content()
176 | }
177 | }
178 |
179 |
180 | /**
181 | * Contains the colors needed to constructs a [TabItem]
182 | */
183 | data class TabsColors(
184 |
185 | val itemRipple: Color,
186 |
187 | val itemIndicator: Color,
188 |
189 |
190 | )
191 |
192 |
193 | /**
194 | * Constructs the default [TabsColors]
195 | *
196 | * @param itemRipple Ripple color for the onclick-animation
197 | * @param itemIndicator Color of the selected item indicator
198 | */
199 | @Composable
200 | fun tabsColors(
201 | itemRipple: Color = MaterialTheme.colorScheme.onSurface,
202 | itemIndicator: Color = MaterialTheme.colorScheme.primary,
203 | ): TabsColors = TabsColors(
204 | itemRipple = itemRipple,
205 | itemIndicator = itemIndicator
206 | )
207 |
208 | /**
209 | * Contains default values for the [TabItem]
210 | */
211 | object TabsDefaults {
212 |
213 | val itemIndicatorHeight = 2.dp
214 |
215 | val itemIndicatorSpacing = 1.dp
216 |
217 | val itemShape = RoundedCornerShape(
218 | size = 26.dp
219 | )
220 |
221 | val itemSubShape = RoundedCornerShape(
222 | size = 23.dp
223 | )
224 |
225 | val itemPadding = PaddingValues(
226 | start = 10.dp,
227 | end = 10.dp,
228 | top = 14.dp,
229 | bottom = 14.dp - itemIndicatorSpacing - itemIndicatorHeight
230 | )
231 |
232 | val itemSubPadding = PaddingValues(
233 | all = 8.dp
234 | )
235 |
236 | val customButtonHeight = 43.dp
237 |
238 | }
--------------------------------------------------------------------------------
/app/src/main/res/values-ru/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Посещаемость
3 | Создать курс
4 | Название курса
5 | Сохранить
6 | Выбрать расписание
7 | ОК
8 | Название не может быть пустым
9 | Добавьте хотя бы один класс для этого курса
10 | Понедельник
11 | Воскресенье
12 | Суббота
13 | Пятница
14 | Четверг
15 | Среда
16 | Вторник
17 | Готово
18 | Отмена
19 |
20 | course_details_card_%1$d
21 | course_details_page
22 |
23 | app_theme
24 | Тема
25 | pure_dark
26 | Pure Dark
27 | material_you_dynamic_color
28 | Динамические цвета
29 | Оформление
30 | default_main_pager_tab
31 | Главный экран
32 | Настройки
33 | Формат времени
34 | Формат даты
35 | %.2f%%
36 |
37 |
38 |
39 | - system
40 | - light
41 | - dark
42 |
43 |
44 | - Как в системе
45 | - Светлая тема
46 | - Темная тема
47 |
48 |
49 |
50 |
51 | - today
52 | - overall
53 |
54 |
55 | - Сегодня
56 | - Общее
57 |
58 |
59 |
60 | - d MMM, yyyy
61 | - EEEE, MMMM d, yyyy
62 | - MM/dd/yyy
63 | - MMM dd, yyyy
64 | - dd MMMM yyy
65 |
66 |
67 | - hh:mm a
68 | - HH:mm
69 |
70 |
71 | - О курсу
72 | - Доп. занятия
73 | - Посещаемость
74 |
75 |
76 |
77 | - День недели
78 | - Начало
79 | - Конец
80 |
81 |
82 |
83 | - Дата
84 | - Начало
85 | - Конец
86 |
87 |
88 | Статус для %1$s с %2$s до %3$s%4$s %5$s
89 | (Доп. занятие)
90 | %1$s
91 | %1$s - %2$s
92 | По расписанию
93 | Доп. занятие
94 |
95 | Удалить курс: %1$s
96 | Вы уверены, что хотите удалить этот курс? Все данные будут навсегда утеряны.
97 |
98 | Нужно посещать: %1$d
99 | %1$s; %2$s - %3$s
100 | Удалить доп. занятие?
101 | Вы уверены, что хотите удалить занятие %1$s с %2$s до %3$s?
102 |
103 | Присутствие: %1$d
104 | Отсутствие: %1$d
105 | Отменено: %1$d
106 |
107 | Редактировать %1$s
108 | Было: %1$s
109 | Было: %d%%
110 | Теперь: %d%%
111 |
112 | Тема
113 | Назад
114 | Внешний вид
115 | Цвет темы
116 | Как в системе
117 | Черный фон
118 | Как в системе
119 | Светлая
120 | Темная
121 | Основной цвет
122 | Формат даты и времени
123 | Главный экран
124 | Занятия сегодня
125 | Другие настройки
126 | О приложении
127 | Исходный код
128 | Автор: Rahul Gill
129 | Очистить
130 | "Нужно посещать: %1$d%%"
131 | Добавить занятие
132 | Удалить занятие
133 | Курсы
134 | Нет занятий сегодня
135 | Нет курсов
136 | Курс %1$s создан
137 | Был
138 | Отсутствовал
139 | Отменено
140 | Не задано
141 | Текущая посещаемость
142 | Расписание
143 | Необходимая посещаемость
144 | Удалено из расписания
145 | Удалить из расписания
146 | Удалить %1$s с %2$s до %3$s
147 | Добавить занятие
148 | Посещаемость %1$s
149 | Удалить запись
150 | Удалить
151 | Время окончания должно быть позже начала
152 | Выберите день, время начала и окончания?
153 | Подсказка
154 | Курс обновлен
155 | Добавить в расписание
156 | Удаление записи также удалит и связанные данные о посещаемости
157 | Удалить и записи посещаемости
158 | Только удалить из расписания
159 | Исключить из расписания
160 | Включить в расписание
161 | Исключено из расписания
162 | Действие
163 | Для непомеченных занятий
164 | Считать присутствием
165 | Считать отсутствием
166 | Ничего не делать
167 | Можно пропустить: %1$d занятий
168 | Нужно посетить: %1$d занятий
169 | Нельзя пропустить следующее
170 | Политика конфиденциальности
171 |
172 |
173 |
174 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Attendance Tracker
3 | Create a course
4 | Course name
5 | Save
6 | Select schedule of classes
7 | OK
8 | Course name can\'t be blank
9 | Add at least one class for this course
10 | Monday
11 | Sunday
12 | Saturday
13 | Friday
14 | Thursday
15 | Wednesday
16 | Tuesday
17 | Done
18 | Cancel
19 |
20 | course_details_card_%1$d
21 | course_details_page
22 |
23 | app_theme
24 | App Theme
25 | pure_dark
26 | Pure Dark in Dark Theme
27 | material_you_dynamic_color
28 | Material You Dynamic Color
29 | Theming
30 | default_main_pager_tab
31 | Default Main Page Tab
32 | Settings
33 | Time Format
34 | Date Format
35 | %.2f%%
36 |
37 |
38 |
39 | - system
40 | - light
41 | - dark
42 |
43 |
44 | - Follow System theme
45 | - Light theme
46 | - Dark theme
47 |
48 |
49 |
50 |
51 | - today
52 | - overall
53 |
54 |
55 | - Today
56 | - Overall
57 |
58 |
59 |
60 | - d MMM, yyyy
61 | - EEEE, MMMM d, yyyy
62 | - MM/dd/yyy
63 | - MMM dd, yyyy
64 | - dd MMMM yyy
65 |
66 |
67 | - hh:mm a
68 | - HH:mm
69 |
70 |
71 | - Course Info
72 | - Extra Classes
73 | - Attendance Record
74 |
75 |
76 |
77 | - Weekday
78 | - Start Time
79 | - End Time
80 |
81 |
82 |
83 | - Date
84 | - Start Time
85 | - End Time
86 |
87 |
88 | Set Attendance Status for %1$s from %2$s to %3$s%4$s %5$s
89 | (Extra class)
90 | on %1$s
91 | %1$s to %2$s
92 | Scheduled Class
93 | Extra Class
94 |
95 | Delete course: %1$s
96 | Do you really want to delete this course item? You\'ll lose all the data associated with this course.
97 |
98 | Required Attendance: %1$d
99 | %1$s; %2$s to %3$s
100 | Delete extra class?
101 | Do you really want to delete the extra class on %1$s from %2$s to %3$s
102 |
103 | Presents: %1$d
104 | Absents: %1$d
105 | Cancelled classes: %1$d
106 |
107 | Edit details for %1$s
108 | Previous name: %1$s
109 | Previous value:: %d%%
110 | Required Attendance updated: %d%%
111 |
112 | App Theme
113 | Go to previous screen
114 | Look and Feel
115 | Custom Color Scheme Seed
116 | Follow System Colors
117 | Pure black background
118 | Follow System
119 | Light
120 | Dark
121 | Color to generate color scheme from
122 | Date Time Formatting
123 | Default Home Tab
124 | Today\'s Classes
125 | Other UI Options
126 | About
127 | Source Code
128 | Author: Rahul Gill
129 | Clear text
130 | "Required Attendance: %1$d%%"
131 | Add Class
132 | Remove Class
133 | Courses
134 | No classes today
135 | No courses added yet
136 | Course %1$s Created
137 | Present
138 | Absent
139 | Cancelled
140 | Not Set
141 | Current Attendance Percentage
142 | Weekly Schedule
143 | Required Attendance Percentage
144 | Deleted schedule item
145 | Delete schedule item
146 | Delete schedule item on %1$s from %2$s to %3$s
147 | Add class on this schedule
148 | %1$s Attendance Records
149 | Delete Record
150 | Delete
151 | End time should come after start time
152 | Select weekday, start time and end time for the new class
153 | Tip
154 | Course Updated
155 | Add schedule item
156 | Deleting schedule item will delete attendance records related to it
157 | Delete related attendance records too
158 | Only delete schedule item
159 | Exclude schedule item from schedule
160 | Include schedule item to schedule
161 | Excluded from schedule
162 | Behaviour
163 | Unset classes behaviour
164 | Consider as presents
165 | Consider as Absents
166 | Do nothing
167 | Can be absent in %1$d classes
168 | Need to attend %1$d more classes
169 | Cannot miss next claas
170 | Privacy Policy
171 |
172 |
173 |
174 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/rahul_gill/attendance/ui/screens/CreateCourseScreen.kt:
--------------------------------------------------------------------------------
1 | package com.github.rahul_gill.attendance.ui.screens
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.ExperimentalLayoutApi
6 | import androidx.compose.foundation.layout.FlowRow
7 | import androidx.compose.foundation.layout.Row
8 | import androidx.compose.foundation.layout.Spacer
9 | import androidx.compose.foundation.layout.fillMaxWidth
10 | import androidx.compose.foundation.layout.height
11 | import androidx.compose.foundation.layout.imePadding
12 | import androidx.compose.foundation.layout.padding
13 | import androidx.compose.foundation.layout.width
14 | import androidx.compose.foundation.rememberScrollState
15 | import androidx.compose.foundation.verticalScroll
16 | import androidx.compose.material.icons.Icons
17 | import androidx.compose.material.icons.automirrored.filled.ArrowBack
18 | import androidx.compose.material.icons.filled.Clear
19 | import androidx.compose.material.icons.filled.Close
20 | import androidx.compose.material3.ExperimentalMaterial3Api
21 | import androidx.compose.material3.ExtendedFloatingActionButton
22 | import androidx.compose.material3.Icon
23 | import androidx.compose.material3.IconButton
24 | import androidx.compose.material3.LargeTopAppBar
25 | import androidx.compose.material3.MaterialTheme
26 | import androidx.compose.material3.OutlinedButton
27 | import androidx.compose.material3.OutlinedCard
28 | import androidx.compose.material3.OutlinedTextField
29 | import androidx.compose.material3.Scaffold
30 | import androidx.compose.material3.Slider
31 | import androidx.compose.material3.SnackbarHost
32 | import androidx.compose.material3.SnackbarHostState
33 | import androidx.compose.material3.Text
34 | import androidx.compose.material3.TopAppBarDefaults
35 | import androidx.compose.material3.rememberTopAppBarState
36 | import androidx.compose.runtime.Composable
37 | import androidx.compose.runtime.getValue
38 | import androidx.compose.runtime.mutableIntStateOf
39 | import androidx.compose.runtime.mutableStateListOf
40 | import androidx.compose.runtime.mutableStateOf
41 | import androidx.compose.runtime.remember
42 | import androidx.compose.runtime.rememberCoroutineScope
43 | import androidx.compose.runtime.saveable.listSaver
44 | import androidx.compose.runtime.saveable.rememberSaveable
45 | import androidx.compose.runtime.setValue
46 | import androidx.compose.runtime.toMutableStateList
47 | import androidx.compose.ui.Alignment
48 | import androidx.compose.ui.Modifier
49 | import androidx.compose.ui.input.nestedscroll.nestedScroll
50 | import androidx.compose.ui.platform.LocalContext
51 | import androidx.compose.ui.platform.testTag
52 | import androidx.compose.ui.res.painterResource
53 | import androidx.compose.ui.res.stringResource
54 | import androidx.compose.ui.unit.dp
55 | import com.github.rahul_gill.attendance.R
56 | import com.github.rahul_gill.attendance.db.DBOps
57 | import com.github.rahul_gill.attendance.ui.comps.AddClassBottomSheet
58 | import com.github.rahul_gill.attendance.db.ClassDetail
59 | import com.github.rahul_gill.attendance.ui.comps.ScheduleItem
60 | import com.github.rahul_gill.attendance.util.timeFormatter
61 | import kotlinx.coroutines.launch
62 |
63 |
64 | @OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
65 | @Composable
66 | fun CreateCourseScreen(
67 | onGoBack: () -> Unit,
68 | onSave: (courseName: String, requiredPercentage: Int, classes: List) -> Unit
69 | ) {
70 | var courseName by rememberSaveable {
71 | mutableStateOf("")
72 | }
73 | var requiredAttendancePercentage by rememberSaveable {
74 | mutableIntStateOf(75)
75 | }
76 | val classesForTheCourse = rememberSaveable(
77 | saver = listSaver(
78 | save = { it.toList() },
79 | restore = { it.toMutableStateList() }
80 | )
81 | ) {
82 | mutableStateListOf()
83 | }
84 |
85 | var showAddClassBottomSheet by rememberSaveable {
86 | mutableStateOf(false)
87 | }
88 | var classToUpdateIndex: Int? by rememberSaveable {
89 | mutableStateOf(null)
90 | }
91 | val scope = rememberCoroutineScope()
92 | val snackbarHostState = remember { SnackbarHostState() }
93 | val context = LocalContext.current
94 |
95 | val scrollBehavior =
96 | TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())
97 | Scaffold(
98 | snackbarHost = {
99 | SnackbarHost(hostState = snackbarHostState)
100 | },
101 | modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
102 | topBar = {
103 | LargeTopAppBar(title = {
104 | Text(
105 | text = stringResource(id = R.string.create_a_course),
106 | )
107 | }, navigationIcon = {
108 | IconButton(onClick = onGoBack, modifier = Modifier.testTag("go_back")) {
109 | Icon(
110 | imageVector = Icons.AutoMirrored.Filled.ArrowBack,
111 | contentDescription = stringResource(id = R.string.go_back_screen)
112 | )
113 | }
114 | }, scrollBehavior = scrollBehavior
115 | )
116 | },
117 | floatingActionButton = {
118 | ExtendedFloatingActionButton(
119 | onClick = onClickSave@{
120 | if (courseName.isBlank()) {
121 | scope.launch {
122 | snackbarHostState.showSnackbar(
123 | message = context.getString(R.string.error_course_name_blank),
124 | withDismissAction = true
125 | )
126 | }
127 | return@onClickSave
128 | }
129 | if (classesForTheCourse.isEmpty()) {
130 | scope.launch {
131 | snackbarHostState.showSnackbar(
132 | message = context.getString(R.string.error_no_classes_for_course),
133 | withDismissAction = true
134 | )
135 | }
136 | return@onClickSave
137 | }
138 | onSave(courseName.trim(), requiredAttendancePercentage, classesForTheCourse.toList())
139 | onGoBack()
140 | },
141 | modifier = Modifier.imePadding()
142 | ) {
143 | Icon(
144 | painter = painterResource(id = R.drawable.baseline_save_24),
145 | contentDescription = null
146 | )
147 | Text(text = stringResource(id = R.string.save))
148 | }
149 | }
150 | ) { innerPadding ->
151 | Column(
152 | modifier = Modifier
153 | .verticalScroll(rememberScrollState())
154 | .padding(innerPadding)
155 | .padding(horizontal = 16.dp)
156 | ) {
157 | Spacer(modifier = Modifier.height(16.dp))
158 | OutlinedTextField(
159 | value = courseName,
160 | onValueChange = { courseName = it },
161 | maxLines = 1,
162 | trailingIcon = {
163 | if (courseName.isNotBlank()) {
164 | IconButton(onClick = { courseName = "" }) {
165 | Icon(
166 | imageVector = Icons.Default.Clear,
167 | contentDescription = stringResource(id = R.string.clear_text)
168 | )
169 | }
170 | }
171 | },
172 | modifier = Modifier.fillMaxWidth().testTag("course_name_input"),
173 | label = {
174 | Text(text = stringResource(id = R.string.course_name))
175 | }
176 | )
177 | Spacer(modifier = Modifier.height(16.dp))
178 | Text(
179 | text = stringResource(
180 | id = R.string.required_attendance_text,
181 | requiredAttendancePercentage
182 | )
183 | )
184 | Spacer(modifier = Modifier.height(8.dp))
185 | Slider(
186 | value = requiredAttendancePercentage.toFloat(),
187 | onValueChange = { requiredAttendancePercentage = it.toInt() },
188 | steps = 100,
189 | valueRange = 1f..100f
190 | )
191 | Spacer(modifier = Modifier.height(16.dp))
192 | Text(
193 | text = stringResource(id = R.string.select_schedule_of_classes),
194 | style = MaterialTheme.typography.titleLarge
195 | )
196 | Column {
197 | classesForTheCourse.forEachIndexed { index, classDetail ->
198 | Spacer(modifier = Modifier.height(8.dp))
199 | ScheduleItem(
200 | item = classDetail,
201 | onClick = {
202 | classToUpdateIndex = index
203 | showAddClassBottomSheet = true
204 | },
205 | onCloseClick = {
206 | classesForTheCourse.removeAt(index)
207 | }
208 | )
209 | }
210 | }
211 |
212 | Spacer(modifier = Modifier.height(8.dp))
213 |
214 | OutlinedButton(onClick = { showAddClassBottomSheet = true },
215 | modifier = Modifier.testTag("add_class_button")) {
216 | Text(text = stringResource(id = R.string.add_class))
217 | }
218 | }
219 | }
220 |
221 | if (showAddClassBottomSheet) {
222 | AddClassBottomSheet(
223 | initialState = classToUpdateIndex?.run { classesForTheCourse[this] },
224 | onDismissRequest = { showAddClassBottomSheet = false },
225 | onCreateClass = { params ->
226 | classToUpdateIndex?.let {
227 | classesForTheCourse[it] = params
228 | classToUpdateIndex = null
229 | } ?: classesForTheCourse.add(params)
230 | }
231 | )
232 | }
233 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/rahul_gill/attendance/ui/comps/Preference.kt:
--------------------------------------------------------------------------------
1 | package com.github.rahul_gill.attendance.ui.comps
2 |
3 |
4 | //import androidx.compose.ui.tooling.preview.Preview
5 | import androidx.compose.foundation.clickable
6 | import androidx.compose.foundation.layout.Arrangement
7 | import androidx.compose.foundation.layout.Box
8 | import androidx.compose.foundation.layout.Column
9 | import androidx.compose.foundation.layout.PaddingValues
10 | import androidx.compose.foundation.layout.Row
11 | import androidx.compose.foundation.layout.Spacer
12 | import androidx.compose.foundation.layout.fillMaxWidth
13 | import androidx.compose.foundation.layout.height
14 | import androidx.compose.foundation.layout.heightIn
15 | import androidx.compose.foundation.layout.padding
16 | import androidx.compose.foundation.layout.width
17 | import androidx.compose.foundation.layout.wrapContentWidth
18 | import androidx.compose.foundation.rememberScrollState
19 | import androidx.compose.foundation.selection.selectable
20 | import androidx.compose.foundation.selection.selectableGroup
21 | import androidx.compose.foundation.shape.RoundedCornerShape
22 | import androidx.compose.foundation.verticalScroll
23 | import androidx.compose.material3.LocalTextStyle
24 | import androidx.compose.material3.MaterialTheme
25 | import androidx.compose.material3.RadioButton
26 | import androidx.compose.material3.Switch
27 | import androidx.compose.material3.Text
28 | import androidx.compose.material3.TextButton
29 | import androidx.compose.material3.minimumInteractiveComponentSize
30 | import androidx.compose.runtime.Composable
31 | import androidx.compose.runtime.getValue
32 | import androidx.compose.runtime.mutableIntStateOf
33 | import androidx.compose.runtime.mutableStateOf
34 | import androidx.compose.runtime.remember
35 | import androidx.compose.runtime.setValue
36 | import androidx.compose.ui.Alignment
37 | import androidx.compose.ui.Modifier
38 | import androidx.compose.ui.draw.clip
39 | import androidx.compose.ui.graphics.Color
40 | import androidx.compose.ui.res.stringResource
41 | import androidx.compose.ui.semantics.Role
42 | import androidx.compose.ui.text.font.FontWeight
43 | import androidx.compose.ui.unit.dp
44 | import androidx.compose.ui.unit.sp
45 | import com.github.rahul_gill.attendance.R
46 |
47 |
48 | private val GroupHeaderStartPadding = 16.dp
49 | private const val GroupHeaderFontSizeMultiplier = 0.85f
50 | private val PrefItemMinHeight = 72.dp
51 |
52 | @Composable
53 | fun PreferenceGroupHeader(
54 | modifier: Modifier = Modifier,
55 | title: String,
56 | color: Color = MaterialTheme.colorScheme.primary
57 | ) {
58 | Box(
59 | Modifier
60 | .padding(start = GroupHeaderStartPadding)
61 | .fillMaxWidth()
62 | .then(modifier),
63 | contentAlignment = Alignment.CenterStart
64 | ) {
65 | Text(
66 | title,
67 | color = color,
68 | fontSize = LocalTextStyle.current.fontSize.times(GroupHeaderFontSizeMultiplier),
69 | fontWeight = FontWeight.SemiBold
70 | )
71 | }
72 | }
73 |
74 | @Composable
75 | fun GenericPreference(
76 | title: String,
77 | modifier: Modifier = Modifier,
78 | leadingIcon: @Composable (() -> Unit)? = null,
79 | trailingContent: @Composable (() -> Unit)? = null,
80 | onClick: (() -> Unit)? = null,
81 | summary: String? = null,
82 | contentPadding: PaddingValues = PaddingValues(24.dp),
83 | placeholderSpaceForLeadingIcon: Boolean = true
84 | ) {
85 | Row(
86 | modifier = Modifier
87 |
88 | .clip(RoundedCornerShape(20))
89 | .clickable(
90 | onClick = onClick ?: {}
91 | )
92 | .heightIn(min = PrefItemMinHeight)
93 | .fillMaxWidth()
94 | .padding(contentPadding)
95 | .then(modifier),
96 | verticalAlignment = Alignment.CenterVertically
97 | ) {
98 | if (leadingIcon == null && placeholderSpaceForLeadingIcon) {
99 | Box(Modifier.minimumInteractiveComponentSize()) {}
100 | } else if (leadingIcon != null) {
101 | Box(
102 | modifier = Modifier.minimumInteractiveComponentSize().align(Alignment.Top),
103 | contentAlignment = Alignment.Center
104 | ) {
105 | leadingIcon()
106 | }
107 | }
108 | Spacer(modifier = Modifier.width(8.dp))
109 | Column(
110 | modifier = Modifier
111 | .weight(1f)
112 | .align(Alignment.CenterVertically)
113 | ) {
114 | Text(
115 | text = title,
116 | fontSize = 19.sp
117 | )
118 | if (summary != null) {
119 | Spacer(modifier = Modifier.height(4.dp))
120 | Text(
121 | text = summary,
122 | fontSize = 15.sp,
123 | modifier = Modifier.wrapContentWidth()
124 | )
125 | }
126 | }
127 | if (trailingContent != null) {
128 | Spacer(
129 | modifier = Modifier
130 | .width(8.dp)
131 | )
132 | trailingContent()
133 | }
134 | }
135 | }
136 |
137 |
138 | @Composable
139 | fun ListPreference(
140 | title: String,
141 | items: List,
142 | selectedItemIndex: Int,
143 | onItemSelection: (Int) -> Unit,
144 | itemToDescription: @Composable (Int) -> String,
145 | modifier: Modifier = Modifier,
146 | leadingIcon: @Composable (() -> Unit)? = null,
147 | placeholderForIcon: Boolean = true,
148 | selectItemOnClick: Boolean = true
149 | ) {
150 | val isShowingSelectionDialog = remember {
151 | mutableStateOf(false)
152 | }
153 | GenericPreference(
154 | title = title,
155 | leadingIcon = leadingIcon,
156 | summary = itemToDescription(selectedItemIndex),
157 | modifier = modifier,
158 | placeholderSpaceForLeadingIcon = placeholderForIcon,
159 | onClick = {
160 | isShowingSelectionDialog.value = true
161 | },
162 | )
163 | if (isShowingSelectionDialog.value) {
164 | var dialogSelectedItemIndex by remember {
165 | mutableIntStateOf(selectedItemIndex)
166 | }
167 | AlertDialog(
168 | onDismissRequest = {
169 | isShowingSelectionDialog.value = false
170 | },
171 | title = {
172 | Text(text = title)
173 | },
174 | body = {
175 | Column(
176 | Modifier
177 | .selectableGroup()
178 | .verticalScroll(rememberScrollState())
179 | ) {
180 | items.forEachIndexed { index, choice ->
181 | Row(
182 | Modifier
183 | .fillMaxWidth()
184 | .height(56.dp)
185 | .selectable(
186 | selected = (index == dialogSelectedItemIndex),
187 | onClick = {
188 | dialogSelectedItemIndex = index
189 | if (selectItemOnClick) {
190 | onItemSelection(index)
191 | }
192 | },
193 | role = Role.RadioButton
194 | )
195 | .padding(horizontal = 16.dp),
196 | verticalAlignment = Alignment.CenterVertically
197 | ) {
198 | RadioButton(
199 | selected = (index == dialogSelectedItemIndex),
200 | onClick = null
201 | )
202 | Text(
203 | text = itemToDescription(index),
204 | style = MaterialTheme.typography.bodyLarge,
205 | modifier = Modifier.padding(start = 16.dp)
206 | )
207 | }
208 | }
209 | }
210 | },
211 | buttonBar = {
212 | Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
213 | TextButton(onClick = { isShowingSelectionDialog.value = false }) {
214 | Text(text = stringResource(id = R.string.cancel))
215 | }
216 | Spacer(modifier = Modifier.width(8.dp))
217 | TextButton(onClick = {
218 | if (!selectItemOnClick) {
219 | onItemSelection(dialogSelectedItemIndex)
220 | }
221 | isShowingSelectionDialog.value = false
222 | }) {
223 | Text(text = stringResource(id = R.string.ok))
224 | }
225 | }
226 | }
227 | )
228 | }
229 | }
230 |
231 | @Composable
232 | fun SwitchPreference(
233 | title: String,
234 | isChecked: Boolean,
235 | modifier: Modifier = Modifier,
236 | summary: String? = null,
237 | onCheckedChange: ((Boolean) -> Unit)? = null,
238 | isEnabled: Boolean = true,
239 | leadingIcon: @Composable (() -> Unit)? = null,
240 | placeholderForIcon: Boolean = true,
241 | ) {
242 | println(" 4523423 SwitchPreference isChecked: $isChecked")
243 | GenericPreference(
244 | title = title,
245 | onClick = {
246 | if (onCheckedChange != null) {
247 |
248 | println(" 4523423 SwitchPreference set isChecked: ${!isChecked}")
249 | onCheckedChange(!isChecked)
250 | }
251 | },
252 | modifier = modifier,
253 | summary = summary,
254 | leadingIcon = leadingIcon,
255 | placeholderSpaceForLeadingIcon = placeholderForIcon,
256 | trailingContent = {
257 |
258 | println("4523423 calling Switch isChecked: $isChecked")
259 | Switch(
260 | modifier = modifier,
261 | checked = isChecked,
262 | onCheckedChange = null,
263 | enabled = isEnabled
264 | )
265 | })
266 | }
267 |
268 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/rahul_gill/attendance/ui/comps/AddClassBottomSheet.kt:
--------------------------------------------------------------------------------
1 | package com.github.rahul_gill.attendance.ui.comps
2 |
3 | import android.text.format.DateFormat
4 | import android.widget.Toast
5 | import androidx.compose.foundation.ExperimentalFoundationApi
6 | import androidx.compose.foundation.layout.Arrangement
7 | import androidx.compose.foundation.layout.Box
8 | import androidx.compose.foundation.layout.Column
9 | import androidx.compose.foundation.layout.PaddingValues
10 | import androidx.compose.foundation.layout.Row
11 | import androidx.compose.foundation.layout.fillMaxWidth
12 | import androidx.compose.foundation.layout.height
13 | import androidx.compose.foundation.layout.heightIn
14 | import androidx.compose.foundation.layout.padding
15 | import androidx.compose.foundation.pager.HorizontalPager
16 | import androidx.compose.foundation.pager.rememberPagerState
17 | import androidx.compose.foundation.selection.selectable
18 | import androidx.compose.foundation.selection.selectableGroup
19 | import androidx.compose.material3.ExperimentalMaterial3Api
20 | import androidx.compose.material3.MaterialTheme
21 | import androidx.compose.material3.RadioButton
22 | import androidx.compose.material3.Text
23 | import androidx.compose.material3.TextButton
24 | import androidx.compose.material3.TimePicker
25 | import androidx.compose.material3.TimePickerState
26 | import androidx.compose.material3.rememberTimePickerState
27 | import androidx.compose.runtime.Composable
28 | import androidx.compose.runtime.LaunchedEffect
29 | import androidx.compose.runtime.getValue
30 | import androidx.compose.runtime.mutableIntStateOf
31 | import androidx.compose.runtime.mutableStateOf
32 | import androidx.compose.runtime.remember
33 | import androidx.compose.runtime.rememberCoroutineScope
34 | import androidx.compose.runtime.saveable.Saver
35 | import androidx.compose.runtime.saveable.rememberSaveable
36 | import androidx.compose.runtime.setValue
37 | import androidx.compose.ui.Alignment
38 | import androidx.compose.ui.Modifier
39 | import androidx.compose.ui.layout.onSizeChanged
40 | import androidx.compose.ui.platform.LocalContext
41 | import androidx.compose.ui.platform.LocalDensity
42 | import androidx.compose.ui.platform.testTag
43 | import androidx.compose.ui.res.stringArrayResource
44 | import androidx.compose.ui.res.stringResource
45 | import androidx.compose.ui.semantics.Role
46 | import androidx.compose.ui.unit.dp
47 | import com.github.rahul_gill.attendance.R
48 | import com.github.rahul_gill.attendance.db.ClassDetail
49 | import kotlinx.coroutines.launch
50 | import java.time.DayOfWeek
51 | import java.time.LocalDate
52 | import java.time.LocalTime
53 | import java.time.format.TextStyle
54 | import java.util.Locale
55 |
56 |
57 | private fun defaultClassDetailWithTimeAdjusted(): ClassDetail {
58 | val start = LocalTime.now().withMinute(0)
59 | return ClassDetail(
60 | startTime = start,
61 | endTime = start.plusHours(1)
62 | )
63 | }
64 |
65 |
66 | @OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
67 | @Composable
68 | fun AddClassBottomSheet(
69 | initialState: ClassDetail? = null,
70 | onDismissRequest: () -> Unit,
71 | onCreateClass: (ClassDetail) -> Unit
72 | ) {
73 | var dayOfWeekSelected by rememberSaveable {
74 | mutableStateOf(initialState?.dayOfWeek ?: LocalDate.now().dayOfWeek)
75 | }
76 | val context = LocalContext.current
77 | val startTimeState = rememberTimePickerState(
78 | initialHour = initialState?.startTime?.hour ?: 0,
79 | initialMinute = initialState?.startTime?.minute ?: 0,
80 | is24Hour = DateFormat.is24HourFormat(context),
81 | )
82 | var endTimeState by rememberSaveable(
83 | saver = Saver(
84 | save = {
85 | listOf(
86 | it.value.hour,
87 | it.value.minute,
88 | it.value.is24hour
89 | )
90 | },
91 | restore = { value ->
92 | mutableStateOf(
93 | TimePickerState(
94 | initialHour = value[0] as Int,
95 | initialMinute = value[1] as Int,
96 | is24Hour = value[2] as Boolean
97 | )
98 | )
99 | }
100 | )
101 | ) {
102 | mutableStateOf(
103 | TimePickerState(
104 | initialHour = initialState?.endTime?.hour ?: 0,
105 | initialMinute = initialState?.endTime?.minute ?: 0,
106 | is24Hour = DateFormat.is24HourFormat(context),
107 | )
108 | )
109 | }
110 | LaunchedEffect(startTimeState.hour, startTimeState.minute) {
111 | endTimeState = TimePickerState(
112 | initialHour = (startTimeState.hour + 1) % 24,
113 | initialMinute = startTimeState.minute,
114 | is24Hour = DateFormat.is24HourFormat(context),
115 | )
116 | }
117 | LaunchedEffect(endTimeState.hour, endTimeState.minute) {
118 | val newEnd = LocalTime.of(endTimeState.hour, endTimeState.minute)
119 | val start = LocalTime.of(startTimeState.hour, startTimeState.minute)
120 | if (newEnd <= start) {
121 | Toast.makeText(
122 | context,
123 | context.getString(R.string.err_end_time_should_be_after_start_time),
124 | Toast.LENGTH_SHORT
125 | ).show()
126 | endTimeState = TimePickerState(
127 | initialHour = (startTimeState.hour + 1) % 24,
128 | initialMinute = startTimeState.minute,
129 | is24Hour = DateFormat.is24HourFormat(context),
130 | )
131 | }
132 | }
133 | BaseDialog(
134 | onDismissRequest = onDismissRequest,
135 | dialogPadding = PaddingValues(0.dp)
136 | ) {
137 | Text(
138 | text = stringResource(R.string.select_weekday_start_time_and_end_time_for_the_new_class),
139 | style = MaterialTheme.typography.titleMedium,
140 | modifier = Modifier.padding(16.dp)
141 | )
142 | val tabs = stringArrayResource(id = R.array.add_schedule_class_bottom_sheet_tabs)
143 | val pagerState = rememberPagerState(pageCount = { tabs.size })
144 | val scope = rememberCoroutineScope()
145 |
146 | Tabs {
147 | tabs.forEachIndexed { index, tabName ->
148 | TabItem(
149 | onClick = { scope.launch { pagerState.animateScrollToPage(index) } },
150 | text = tabName,
151 | selected = pagerState.currentPage == index
152 | )
153 | }
154 | }
155 | var pagerMinSize by remember {
156 | mutableIntStateOf(0)
157 | }
158 | HorizontalPager(
159 | state = pagerState,
160 | modifier = Modifier.heightIn(min = with(LocalDensity.current) { pagerMinSize.toDp() })
161 | ) { page ->
162 | when (page) {
163 | 0 -> {
164 | Column(
165 | Modifier
166 | .selectableGroup()
167 | .onSizeChanged { pagerMinSize = maxOf(pagerMinSize, it.height) }) {
168 | DayOfWeek.entries.forEach { dayOfWeek ->
169 | Row(
170 | Modifier
171 | .fillMaxWidth()
172 | .height(56.dp)
173 | .selectable(
174 | selected = (dayOfWeekSelected == dayOfWeek),
175 | onClick = { dayOfWeekSelected = dayOfWeek },
176 | role = Role.RadioButton
177 | )
178 | .padding(horizontal = 16.dp),
179 | verticalAlignment = Alignment.CenterVertically
180 | ) {
181 | RadioButton(
182 | selected = (dayOfWeek == dayOfWeekSelected),
183 | onClick = null // null recommended for accessibility with screenreaders
184 | )
185 | Text(
186 | text = dayOfWeek.getDisplayName(
187 | TextStyle.FULL,
188 | Locale.getDefault()
189 | ),
190 | style = MaterialTheme.typography.bodyLarge,
191 | modifier = Modifier.padding(start = 16.dp)
192 | )
193 | }
194 | }
195 | }
196 | }
197 | 1 -> {
198 | Box(
199 | modifier = Modifier
200 | .fillMaxWidth()
201 | .onSizeChanged { pagerMinSize = maxOf(pagerMinSize, it.height) },
202 | contentAlignment = Alignment.Center
203 | ) {
204 | TimePicker(state = startTimeState)
205 | }
206 | }
207 | 2 -> {
208 | Box(
209 | modifier = Modifier
210 | .fillMaxWidth()
211 | .onSizeChanged { pagerMinSize = maxOf(pagerMinSize, it.height) },
212 | contentAlignment = Alignment.Center
213 | ) {
214 | TimePicker(state = endTimeState)
215 | }
216 | }
217 | }
218 | }
219 | Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
220 | TextButton(onClick = onDismissRequest) {
221 | Text(text = stringResource(id = R.string.cancel))
222 | }
223 | TextButton(onClick = {
224 | onCreateClass(
225 | ClassDetail(
226 | dayOfWeek = dayOfWeekSelected,
227 | startTime = LocalTime.of(startTimeState.hour, startTimeState.minute),
228 | endTime = LocalTime.of(endTimeState.hour, endTimeState.minute)
229 | )
230 | )
231 | onDismissRequest()
232 | }, modifier = Modifier.testTag("sheet_add_class_button")
233 | ) {
234 | Text(text = stringResource(id = R.string.ok))
235 | }
236 | }
237 | }
238 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/github/rahul_gill/attendance/ScreenGrabTest.kt:
--------------------------------------------------------------------------------
1 | package com.github.rahul_gill.attendance
2 |
3 | import androidx.activity.ComponentActivity
4 | import androidx.compose.foundation.layout.fillMaxSize
5 | import androidx.compose.material3.MaterialTheme
6 | import androidx.compose.material3.Surface
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.graphics.Color
9 | import androidx.compose.ui.input.key.Key
10 | import androidx.compose.ui.test.ExperimentalTestApi
11 | import androidx.compose.ui.test.junit4.AndroidComposeTestRule
12 | import androidx.compose.ui.test.junit4.ComposeContentTestRule
13 | import androidx.compose.ui.test.junit4.createComposeRule
14 | import androidx.compose.ui.test.onNodeWithTag
15 | import androidx.compose.ui.test.onNodeWithText
16 | import androidx.compose.ui.test.performClick
17 | import androidx.compose.ui.test.performKeyInput
18 | import androidx.compose.ui.test.pressKey
19 | import androidx.test.ext.junit.rules.ActivityScenarioRule
20 | import androidx.test.ext.junit.runners.AndroidJUnit4
21 | import com.github.rahul_gill.attendance.db.AttendanceCounts
22 | import com.github.rahul_gill.attendance.db.AttendanceRecordHybrid
23 | import com.github.rahul_gill.attendance.db.ClassDetail
24 | import com.github.rahul_gill.attendance.db.CourseClassStatus
25 | import com.github.rahul_gill.attendance.db.CourseDetailsOverallItem
26 | import com.github.rahul_gill.attendance.db.DBOps
27 | import com.github.rahul_gill.attendance.db.ExtraClassTimings
28 | import com.github.rahul_gill.attendance.prefs.PreferenceManager
29 | import com.github.rahul_gill.attendance.prefs.UnsetClassesBehavior
30 | import com.github.rahul_gill.attendance.ui.RootNavHost
31 | import com.github.rahul_gill.attendance.ui.comps.AttendanceAppTheme
32 | import com.github.rahul_gill.attendance.ui.comps.ColorSchemeType
33 | import kotlinx.coroutines.delay
34 | import kotlinx.coroutines.flow.first
35 | import kotlinx.coroutines.flow.firstOrNull
36 | import kotlinx.coroutines.runBlocking
37 | import org.junit.Before
38 | import org.junit.Rule
39 | import org.junit.Test
40 | import org.junit.runner.RunWith
41 | import tools.fastlane.screengrab.Screengrab
42 | import tools.fastlane.screengrab.UiAutomatorScreenshotStrategy
43 | import tools.fastlane.screengrab.locale.LocaleTestRule
44 | import java.time.DayOfWeek
45 | import java.time.LocalDate
46 | import java.time.LocalTime
47 | import java.time.temporal.TemporalAdjusters
48 |
49 |
50 | @RunWith(AndroidJUnit4::class)
51 | class ScreenGrabTest {
52 | @get:Rule
53 | val composeTestRule = createComposeRule()
54 |
55 | @Rule
56 | @JvmField
57 | val localeTestRule = LocaleTestRule()
58 |
59 | @Before
60 | fun init() {
61 | val dbOps = DBOps.instance
62 | Screengrab.setDefaultScreenshotStrategy(UiAutomatorScreenshotStrategy())
63 | courses.forEach {
64 | dbOps.createCourse(
65 | name = it.courseName,
66 | requiredAttendancePercentage = it.requiredAttendance,
67 | schedule = listOf(
68 | ClassDetail(
69 | dayOfWeek = DayOfWeek.MONDAY,
70 | ),
71 | ClassDetail(
72 | dayOfWeek = DayOfWeek.WEDNESDAY,
73 | )
74 | )
75 | )
76 | }
77 | runBlocking {
78 | dbOps.getCoursesDetailsList().firstOrNull()?.let { courses ->
79 | courses.forEachIndexed { courseIndex, course ->
80 | val schedule = dbOps.getScheduleClassesForCourse(course.courseId).first()
81 | val dateX = LocalDate.now()
82 | .with(TemporalAdjusters.firstInMonth(schedule.first().dayOfWeek))
83 | for (i in 0..10) {
84 | dbOps.markAttendanceForScheduleClass(
85 | attendanceId = null,
86 | classStatus = if (course.courseName == "Chemistry" && (i == 2 || i == 4))
87 | CourseClassStatus.Absent
88 | else CourseClassStatus.Present,
89 | scheduleId = schedule.first().scheduleId,
90 | date = dateX.minusWeeks(i.toLong()),
91 | courseId = course.courseId
92 | )
93 | }
94 | if (courseIndex % 2 == 1) {
95 | dbOps.createExtraClasses(
96 | courseId = course.courseId,
97 | timings = ExtraClassTimings(
98 | date = LocalDate.now(),
99 | startTime = LocalTime.of(13, 0),
100 | endTime = LocalTime.of(14, 0)
101 | )
102 | )
103 | } else {
104 | dbOps.markAttendanceForScheduleClass(
105 | attendanceId = null,
106 | classStatus = CourseClassStatus.Unset,
107 | scheduleId = schedule.first().scheduleId,
108 | date = LocalDate.now(),
109 | courseId = course.courseId
110 | )
111 | }
112 | }
113 | }
114 | }
115 | }
116 |
117 | @OptIn(ExperimentalTestApi::class)
118 | @Test
119 | fun mainScreenScreenshots() {
120 | composeTestRule.setContent {
121 | AttendanceAppTheme(
122 | colorSchemeType = ColorSchemeType.WithSeed(Color.Green)
123 | ) {
124 | Surface(
125 | modifier = Modifier
126 | .fillMaxSize(),
127 | color = MaterialTheme.colorScheme.background
128 | ) {
129 | RootNavHost(
130 | )
131 | }
132 | }
133 | }
134 | //main screen
135 | composeTestRule.takeScreenshot("1_main_screen_today")
136 | composeTestRule.onNodeWithTag(testTag = "courses_button").performClick()
137 | composeTestRule.takeScreenshot("2_main_screen_overall_courses")
138 | //course details
139 | composeTestRule.onNodeWithText(text = "Chemistry").performClick()
140 | composeTestRule.takeScreenshot("3_course_details")
141 | composeTestRule.onNodeWithTag(testTag = "go_back").performClick()
142 | //create course
143 | composeTestRule.onNodeWithTag(testTag = "create_course_button").performClick()
144 | composeTestRule.onNodeWithTag(testTag = "course_name_input").run {
145 | performClick()
146 | performKeyInput { listOf(Key.M, Key.A, Key.T, Key.H, Key.S).forEach { pressKey(it) } }
147 | }
148 | composeTestRule.onNodeWithTag(testTag = "add_class_button").performClick()
149 | composeTestRule.onNodeWithTag(testTag = "sheet_add_class_button").performClick()
150 | composeTestRule.takeScreenshot("4_create_course")
151 | composeTestRule.onNodeWithTag(testTag = "go_back").performClick()
152 | //settings
153 | composeTestRule.onNodeWithTag(testTag = "go_to_settings").performClick()
154 | composeTestRule.takeScreenshot("5_settings")
155 | }
156 |
157 |
158 | private val courses = listOf(
159 | "Physics",
160 | "Chemistry",
161 | "Social",
162 | "Fighting lab",
163 | "French",
164 | "Mathematics",
165 | "English",
166 | "Politics"
167 | ).mapIndexed { indx, name ->
168 | val presents = 10
169 | val absents = if (indx == 4) 2 else 0
170 | val cancels = 0
171 | CourseDetailsOverallItem(
172 | courseId = indx.toLong(),
173 | courseName = name,
174 | requiredAttendance = 75.0,
175 | currentAttendancePercentage = 100.0 * presents / (presents + absents),
176 | presents = presents.toInt(),
177 | absents = absents.toInt(),
178 | cancels = cancels.toInt()
179 | )
180 | }
181 | private val demoTodayClasses: List>
182 | get() {
183 | val date = LocalDate.now().with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY))
184 | var startTime = LocalTime.of(10, 0)
185 | val endTime = { startTime.plusHours(1) }
186 | val list = mutableListOf()
187 |
188 | for (i in 0..7) {
189 | if (i == 5) {
190 | AttendanceRecordHybrid.ExtraClass(
191 | extraClassId = i.toLong(),
192 | startTime = startTime,
193 | endTime = endTime(),
194 | courseName = courses[i].courseName,
195 | date = date,
196 | classStatus = CourseClassStatus.Present,
197 | courseId = i.toLong()
198 | )
199 | } else {
200 | list.add(
201 | AttendanceRecordHybrid.ScheduledClass(
202 | attendanceId = i.toLong(),
203 | scheduleId = i.toLong(),
204 | startTime = startTime,
205 | endTime = endTime(),
206 | courseName = courses[i].courseName,
207 | date = date,
208 | classStatus = if (i == 2) CourseClassStatus.Absent else CourseClassStatus.Present,
209 | courseId = i.toLong()
210 | )
211 | )
212 | }
213 | startTime = startTime.plusHours(1)
214 | }
215 |
216 |
217 | return list.map { record ->
218 | Pair(
219 | record,
220 | courses.find { it.courseId == record.courseId }!!.run {
221 | val presentsLater =
222 | (presents + if (PreferenceManager.unsetClassesBehavior.value == UnsetClassesBehavior.ConsiderPresent)
223 | unsets else 0).toLong()
224 | val absentsLater =
225 | (absents + if (PreferenceManager.unsetClassesBehavior.value == UnsetClassesBehavior.ConsiderAbsent)
226 | unsets else 0).toLong()
227 | AttendanceCounts(
228 | if (absentsLater + presentsLater == 0L) 100.0 else 100.0 * presentsLater / (absentsLater + presentsLater),
229 | presentsLater,
230 | absentsLater,
231 | cancels.toLong(),
232 | unsets.toLong(),
233 | requiredAttendance
234 | )
235 | }
236 | )
237 | }
238 | }
239 |
240 | private fun AndroidComposeTestRule, T>.takeScreenshot(
241 | screenshotName: String
242 | ) {
243 | runBlocking {
244 | awaitIdle()
245 | delay(100)
246 | Screengrab.screenshot(screenshotName)
247 | }
248 | }
249 |
250 | private fun ComposeContentTestRule.takeScreenshot(
251 | screenshotName: String
252 | ) {
253 | runBlocking {
254 | awaitIdle()
255 | delay(100)
256 | Screengrab.screenshot(screenshotName)
257 | }
258 | }
259 | }
--------------------------------------------------------------------------------
/app/src/main/sqldelight/com/github/rahul_gill/attendance/app.sq:
--------------------------------------------------------------------------------
1 | import com.github.rahul_gill.attendance.db.CourseClassStatus;
2 | import java.time.DayOfWeek;
3 | import java.time.LocalDate;
4 | import java.time.LocalTime;
5 |
6 | CREATE TABLE Course (
7 | courseId INTEGER PRIMARY KEY AUTOINCREMENT,
8 | courseName TEXT NOT NULL,
9 | requiredAttendance REAL NOT NULL
10 | );
11 |
12 | CREATE TABLE Schedule (
13 | scheduleId INTEGER PRIMARY KEY AUTOINCREMENT,
14 | courseId INTEGER NOT NULL,
15 | weekday INTEGER AS DayOfWeek NOT NULL,
16 | startTime TEXT AS LocalTime NOT NULL,
17 | endTime TEXT AS LocalTime NOT NULL,
18 | includedInSchedule INTEGER NOT NULL DEFAULT 0,
19 | CONSTRAINT fk_course
20 | FOREIGN KEY (courseId)
21 | REFERENCES Course (courseId)
22 | ON DELETE CASCADE
23 | );
24 |
25 | CREATE TABLE Attendance (
26 | attendanceId INTEGER PRIMARY KEY AUTOINCREMENT,
27 | scheduleId INTEGER,
28 | courseId INTEGER,
29 | classStatus TEXT AS CourseClassStatus NOT NULL,
30 | date TEXT AS LocalDate NOT NULL,
31 | CONSTRAINT fk_schedule
32 | FOREIGN KEY (scheduleId)
33 | REFERENCES Schedule (scheduleId)
34 | ON DELETE CASCADE,
35 | CONSTRAINT fk_course
36 | FOREIGN KEY (courseId)
37 | REFERENCES Course (courseId)
38 | ON DELETE CASCADE
39 | );
40 |
41 | CREATE TABLE ExtraClasses (
42 | extraClassId INTEGER PRIMARY KEY AUTOINCREMENT,
43 | courseId INTEGER NOT NULL,
44 | date TEXT AS LocalDate NOT NULL,
45 | startTime TEXT AS LocalTime NOT NULL,
46 | endTime TEXT AS LocalTime NOT NULL,
47 | classStatus TEXT AS CourseClassStatus NOT NULL,
48 | CONSTRAINT fk_course
49 | FOREIGN KEY (courseId)
50 | REFERENCES Course (courseId)
51 | ON DELETE CASCADE
52 | );
53 |
54 | getCourseListForToday:
55 | SELECT Attendance.attendanceId, Schedule.scheduleId, Course.courseId, Course.courseName, Schedule.startTime, Schedule.endTime,
56 | CASE WHEN Attendance.classStatus IS NULL THEN 'Unset'
57 | ELSE Attendance.classStatus
58 | END AS classStatus,
59 | Attendance.date
60 | FROM Schedule
61 | JOIN Course ON Schedule.courseId = Course.courseId AND Schedule.weekday = strftime('%w', 'now')
62 | LEFT JOIN Attendance ON Schedule.scheduleId = Attendance.scheduleId AND Attendance.date = DATE('now', 'localtime')
63 | WHERE Schedule.includedInSchedule <> 0
64 | AND DATE('now', 'localtime') = Attendance.date
65 | OR (Attendance.scheduleId IS NULL AND DATE('now', 'localtime') = DATE('now', 'localtime'));
66 |
67 | getExtraClassesListForToday:
68 | SELECT Course.courseId, Course.courseName, ExtraClasses.startTime, ExtraClasses.endTime, ExtraClasses.classStatus, ExtraClasses.extraClassId, ExtraClasses.date
69 | FROM Course
70 | JOIN ExtraClasses ON Course.courseId = ExtraClasses.courseId
71 | WHERE ExtraClasses.date = DATE('now', 'localtime');
72 |
73 | changeActivateStatusOfScheduleItem:
74 | UPDATE Schedule
75 | SET includedInSchedule = :activate
76 | WHERE scheduleId = :scheduleId;
77 |
78 |
79 |
80 | createCourse:
81 | INSERT INTO Course (courseName, requiredAttendance)
82 | VALUES ( ?, ?);
83 |
84 | udpateCourse:
85 | UPDATE Course SET courseName = ?, requiredAttendance = ?
86 | WHERE courseId = ?;
87 |
88 | deleteScheduleItem:
89 | DELETE FROM Schedule WHERE Schedule.scheduleId = ?;
90 |
91 | deleteAttendanceRecordsOnSchedule:
92 | DELETE FROM Attendance WHERE Attendance.scheduleId = ?;
93 |
94 |
95 |
96 | getLastInsertRowID:
97 | SELECT last_insert_rowid();
98 |
99 | createScheduleItemForCourse:
100 | INSERT INTO Schedule (courseId, weekday, startTime, endTime, includedInSchedule)
101 | VALUES ( ?, ?, ?, ?, ?);
102 |
103 | updateScheduleItemForCourse:
104 | UPDATE Schedule
105 | SET weekday = ?, startTime = ?, endTime = ?, includedInSchedule = ?
106 | WHERE scheduleId = ?;
107 |
108 | scheduleExists:
109 | SELECT COUNT(*) FROM Schedule WHERE scheduleId = ?;
110 |
111 | getCourseDetails:
112 | SELECT
113 | Course.courseId,
114 | Course.courseName,
115 | Course.requiredAttendance,
116 | GROUP_CONCAT(Schedule.weekday || ' ' || Schedule.startTime || ' ' || Schedule.endTime) AS scheduleDetails,
117 | COALESCE((SELECT COUNT(*) FROM Attendance WHERE Attendance.scheduleId IN (SELECT scheduleId FROM Schedule WHERE Schedule.courseId = Course.courseId) AND Attendance.classStatus = 'Present') / NULLIF((SELECT COUNT(*) FROM Attendance WHERE Attendance.scheduleId IN (SELECT scheduleId FROM Schedule WHERE Schedule.courseId = Course.courseId)), 0) * 100, 0) AS currentAttendancePercentage,
118 | (SELECT COUNT(*) FROM Schedule WHERE Schedule.courseId = Course.courseId AND Schedule.weekday = strftime('%w', 'now')) AS hasClassesToday,
119 | (SELECT GROUP_CONCAT(startTime || '-' || endTime) FROM Schedule WHERE Schedule.courseId = Course.courseId AND Schedule.weekday = strftime('%w', 'now')) AS classesToday
120 | FROM
121 | Course
122 | JOIN
123 | Schedule ON Course.courseId = Schedule.courseId
124 | WHERE
125 | Schedule.includedInSchedule = 1 AND Course.courseId = ?
126 | GROUP BY
127 | Course.courseName, Course.requiredAttendance;
128 |
129 | -- TODO: take into account unset items which have a date before today (or not or both with two queries)
130 | getCoursesDetailsList:
131 | SELECT
132 | Course.courseId,
133 | Course.courseName,
134 | Course.requiredAttendance,
135 | (SELECT COUNT(*) FROM Schedule WHERE Schedule.courseId = Course.courseId AND Schedule.weekday = strftime('%w', 'now')) AS numClassesToday,
136 | ((SELECT COUNT(*) AS presents FROM Attendance WHERE scheduleId IN (SELECT scheduleId FROM Schedule WHERE courseId = Course.courseId) AND classStatus = 'Present')
137 | + (SELECT COUNT(*) AS presents FROM ExtraClasses WHERE courseId = Course.courseId AND classStatus = 'Present')) AS nPresents,
138 | ((SELECT COUNT(*) AS presents FROM Attendance WHERE scheduleId IN (SELECT scheduleId FROM Schedule WHERE courseId = Course.courseId) AND classStatus = 'Absent')
139 | + (SELECT COUNT(*) AS presents FROM ExtraClasses WHERE courseId = Course.courseId AND classStatus = 'Absent')) AS nAbsents,
140 | ((SELECT COUNT(*) AS presents FROM Attendance WHERE scheduleId IN (SELECT scheduleId FROM Schedule WHERE courseId = Course.courseId) AND classStatus = 'Cancelled')
141 | + (SELECT COUNT(*) AS presents FROM ExtraClasses WHERE courseId = Course.courseId AND classStatus = 'Cancelled')) AS nCancels,
142 | ((SELECT COUNT(*) AS presents FROM Attendance WHERE scheduleId IN (SELECT scheduleId FROM Schedule WHERE courseId = Course.courseId) AND classStatus = 'Unset')
143 | + (SELECT COUNT(*) AS presents FROM ExtraClasses WHERE courseId = Course.courseId AND classStatus = 'Unset')) AS nUnsets
144 | FROM
145 | Course;
146 |
147 | getCoursesDetailsWithId:
148 | SELECT
149 | Course.courseId,
150 | Course.courseName,
151 | Course.requiredAttendance,
152 | (SELECT COUNT(*) FROM Schedule WHERE Schedule.courseId = Course.courseId AND Schedule.weekday = strftime('%w', 'now')) AS numClassesToday,
153 | ((SELECT COUNT(*) AS presents FROM Attendance WHERE scheduleId IN (SELECT scheduleId FROM Schedule WHERE courseId = Course.courseId) AND classStatus = 'Present')
154 | + (SELECT COUNT(*) AS presents FROM ExtraClasses WHERE courseId = Course.courseId AND classStatus = 'Present')) AS nPresents,
155 | ((SELECT COUNT(*) AS presents FROM Attendance WHERE scheduleId IN (SELECT scheduleId FROM Schedule WHERE courseId = Course.courseId) AND classStatus = 'Absent')
156 | + (SELECT COUNT(*) AS presents FROM ExtraClasses WHERE courseId = Course.courseId AND classStatus = 'Absent')) AS nAbsents,
157 | ((SELECT COUNT(*) AS presents FROM Attendance WHERE scheduleId IN (SELECT scheduleId FROM Schedule WHERE courseId = Course.courseId) AND classStatus = 'Cancelled')
158 | + (SELECT COUNT(*) AS presents FROM ExtraClasses WHERE courseId = Course.courseId AND classStatus = 'Cancelled')) AS nCancels,
159 | ((SELECT COUNT(*) AS presents FROM Attendance WHERE scheduleId IN (SELECT scheduleId FROM Schedule WHERE courseId = Course.courseId) AND classStatus = 'Unset')
160 | + (SELECT COUNT(*) AS presents FROM ExtraClasses WHERE courseId = Course.courseId AND classStatus = 'Unset')) AS nUnsets
161 | FROM
162 | Course
163 | WHERE Course.courseId = ?;
164 |
165 | getScheduleClassesForCourse:
166 | SELECT * FROM Schedule WHERE Schedule.courseId = ? ORDER BY Schedule.includedInSchedule, Schedule.weekday, Schedule.startTime;
167 |
168 | getExtraClasssesForCourse:
169 | SELECT * FROM ExtraClasses WHERE ExtraClasses.courseId = ? ORDER BY ExtraClasses.date DESC, ExtraClasses.startTime;
170 |
171 | createExtraClass:
172 | INSERT INTO ExtraClasses(courseId, date, startTime, endTime, classStatus)
173 | VALUES (?, ?, ?, ?, ?);
174 |
175 | updateExtraClassStatus:
176 | UPDATE ExtraClasses
177 | SET classStatus = :status
178 | WHERE extraClassId = :extraClassId;
179 |
180 | markAttendance:
181 | INSERT OR REPLACE INTO Attendance (attendanceId, classStatus, scheduleId, date, courseId)
182 | VALUES (?, ?, ?, ?, ?);
183 |
184 | markAttendanceInsert:
185 | INSERT INTO Attendance (classStatus, scheduleId, date, courseId)
186 | VALUES ( ?, ?, ?, ?);
187 |
188 |
189 | markAttendanceUpdate:
190 | UPDATE Attendance SET classStatus = :status WHERE attendanceId = :attendanceId;
191 |
192 | checkOverlappingSchedule:
193 | SELECT COUNT(*) = 0
194 | FROM Schedule
195 | WHERE :weekDay = Schedule.weekday AND(
196 | --completely inside
197 | (:newStartTime >= startTime AND :newStartTime <= endTime) OR
198 | --overlapping on left side
199 | (:newEndTime > startTime AND :newEndTime <= endTime) OR
200 | --overlapping on left side
201 | (:newStartTime >= startTime AND :newStartTime < endTime) OR
202 | --other entry completely inside it
203 | (:newStartTime <= startTime AND :newEndTime >= endTime)
204 | );
205 |
206 |
207 | deleteCourse:
208 | DELETE FROM Course WHERE Course.courseId = ?;
209 |
210 | deleteExtraClass:
211 | DELETE FROM ExtraClasses WHERE ExtraClasses.extraClassId = ?;
212 |
213 | deleteScheduleAttendanceRecord:
214 | DELETE FROM Attendance WHERE Attendance.attendanceId = ?;
215 |
216 |
217 | markedAttendancesForCourse:
218 | SELECT a.attendanceId AS entityId, a.scheduleId AS scheduleId, a.date, s.startTime, s.endTime, a.classStatus, 0 AS isExtraCLass, course.courseName, course.courseId
219 | FROM Attendance a, Schedule s, Course course
220 | WHERE a.scheduleId = s.scheduleId AND s.courseId = :courseId AND s.courseId = course.courseId
221 | UNION
222 | SELECT e.extraClassId AS entityId, NULL AS scheduleId, e.date, e.startTime, e.endTime, e.classStatus, 1 AS isExtraCLass , course.courseName, course.courseId
223 | FROM ExtraClasses e, Course course
224 | WHERE e.courseId = :courseId AND e.courseId = course.courseId;
225 |
226 | getCourseDetailsSingle:
227 | SELECT
228 | ((SELECT COUNT(*) AS presents FROM Attendance WHERE scheduleId IN (SELECT scheduleId FROM Schedule WHERE courseId = Course.courseId) AND classStatus = 'Present')
229 | + (SELECT COUNT(*) AS presents FROM ExtraClasses WHERE courseId = Course.courseId AND classStatus = 'Present')) AS nPresents,
230 | ((SELECT COUNT(*) AS presents FROM Attendance WHERE scheduleId IN (SELECT scheduleId FROM Schedule WHERE courseId = Course.courseId) AND classStatus = 'Absent')
231 | + (SELECT COUNT(*) AS presents FROM ExtraClasses WHERE courseId = Course.courseId AND classStatus = 'Absent')) AS nAbsents,
232 | ((SELECT COUNT(*) AS presents FROM Attendance WHERE scheduleId IN (SELECT scheduleId FROM Schedule WHERE courseId = Course.courseId) AND classStatus = 'Cancelled')
233 | + (SELECT COUNT(*) AS presents FROM ExtraClasses WHERE courseId = Course.courseId AND classStatus = 'Cancelled')) AS nCancels,
234 |
235 | ((SELECT COUNT(*) AS presents FROM Attendance WHERE scheduleId IN (SELECT scheduleId FROM Schedule WHERE courseId = Course.courseId) AND classStatus = 'Unset')
236 | + (SELECT COUNT(*) AS presents FROM ExtraClasses WHERE courseId = Course.courseId AND classStatus = 'Unset')) AS nUnsets,
237 | Course.requiredAttendance
238 | FROM Course
239 | WHERE courseId = ?;
240 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/rahul_gill/attendance/ui/comps/Popup.kt:
--------------------------------------------------------------------------------
1 | package com.github.rahul_gill.attendance.ui.comps
2 |
3 |
4 | import androidx.compose.animation.core.MutableTransitionState
5 | import androidx.compose.animation.core.animateFloat
6 | import androidx.compose.animation.core.rememberTransition
7 | import androidx.compose.animation.core.tween
8 | import androidx.compose.animation.core.updateTransition
9 | import androidx.compose.foundation.background
10 | import androidx.compose.foundation.border
11 | import androidx.compose.foundation.clickable
12 | import androidx.compose.foundation.interaction.MutableInteractionSource
13 | import androidx.compose.foundation.layout.Arrangement
14 | import androidx.compose.foundation.layout.Box
15 | import androidx.compose.foundation.layout.Column
16 | import androidx.compose.foundation.layout.ColumnScope
17 | import androidx.compose.foundation.layout.IntrinsicSize
18 | import androidx.compose.foundation.layout.PaddingValues
19 | import androidx.compose.foundation.layout.Row
20 | import androidx.compose.foundation.layout.Spacer
21 | import androidx.compose.foundation.layout.fillMaxWidth
22 | import androidx.compose.foundation.layout.padding
23 | import androidx.compose.foundation.layout.width
24 | import androidx.compose.foundation.shape.RoundedCornerShape
25 | import androidx.compose.material.ripple.rememberRipple
26 | import androidx.compose.material3.LocalContentColor
27 | import androidx.compose.material3.MaterialTheme
28 | import androidx.compose.material3.Text
29 | import androidx.compose.material3.ripple
30 | import androidx.compose.runtime.Composable
31 | import androidx.compose.runtime.CompositionLocalProvider
32 | import androidx.compose.runtime.getValue
33 | import androidx.compose.runtime.remember
34 | import androidx.compose.ui.Alignment
35 | import androidx.compose.ui.Modifier
36 | import androidx.compose.ui.draw.alpha
37 | import androidx.compose.ui.draw.clip
38 | import androidx.compose.ui.draw.shadow
39 | import androidx.compose.ui.graphics.Color
40 | import androidx.compose.ui.graphics.graphicsLayer
41 | import androidx.compose.ui.platform.LocalDensity
42 | import androidx.compose.ui.semantics.Role
43 | import androidx.compose.ui.unit.dp
44 | import androidx.compose.ui.window.Popup
45 | import androidx.compose.ui.window.PopupPositionProvider
46 | import androidx.compose.ui.window.PopupProperties
47 |
48 | /**
49 | * Composable for a oneui-style popup emnu for selecting different kind of items.
50 | * Can be used in spinners or other menus
51 | *
52 | * TODO: Exit animation is not playing due to implementation struggles
53 | *
54 | * @param modifier The [Modifier] to apply to the container
55 | * @param colors The [MenuColors] to apply
56 | * @param visible Whether the menu is currently visible
57 | * @param onDismissRequest Callback for when the menu is dismissed
58 | * @param properties The [PopupProperties] to apply
59 | * @param content The content to put inside the Menu. Preferably [SelectableMenuItem]s. Arranged along the y-Axis
60 | */
61 | @Composable
62 | fun PopupMenu(
63 | modifier: Modifier = Modifier,
64 | colors: MenuColors = menuColors(),
65 | visible: Boolean = true,
66 | onDismissRequest: () -> Unit,
67 | properties: PopupProperties = PopupProperties(
68 | focusable = true
69 | ),
70 | content: @Composable ColumnScope.() -> Unit
71 | ) {
72 | Popup(
73 | onDismissRequest = onDismissRequest,
74 | properties = properties
75 | ) {
76 | PopupContent(
77 | modifier = modifier,
78 | visible = visible,
79 | colors = colors
80 | ) {
81 | content()
82 | }
83 | }
84 | }
85 |
86 |
87 | /**
88 | * Overload that takes in a [PopupPositionProvider]
89 | * Can be used in spinners or other menus
90 | *
91 | * TODO: Exit animation is not playing due to implementation struggles
92 | *
93 | * @param modifier The [Modifier] to apply to the container
94 | * @param colors The [MenuColors] to apply
95 | * @param visible Whether the menu is currently visible
96 | * @param onDismissRequest Callback for when the menu is dismissed
97 | * @param properties The [PopupProperties] to apply
98 | * @param popupPositionProvider The [PopupPositionProvider] to position the popup
99 | * @param content The content to put inside the Menu. Preferably [SelectableMenuItem]s. Arranged along the y-Axis
100 | */
101 | @Composable
102 | fun PopupMenu(
103 | modifier: Modifier = Modifier,
104 | colors: MenuColors = menuColors(),
105 | visible: Boolean = true,
106 | onDismissRequest: () -> Unit,
107 | properties: PopupProperties = PopupProperties(
108 | focusable = true
109 | ),
110 | popupPositionProvider: PopupPositionProvider,
111 | content: @Composable ColumnScope.() -> Unit
112 | ) {
113 | Popup(
114 | onDismissRequest = onDismissRequest,
115 | properties = properties,
116 | popupPositionProvider = popupPositionProvider
117 | ) {
118 | PopupContent(
119 | modifier = modifier,
120 | visible = visible,
121 | colors = colors
122 | ) {
123 | content()
124 | }
125 | }
126 | }
127 |
128 | @Composable
129 | private fun PopupContent(
130 | modifier: Modifier = Modifier,
131 | visible: Boolean,
132 | colors: MenuColors,
133 | content: @Composable ColumnScope.() -> Unit
134 | ) {
135 | val expandedState = remember { MutableTransitionState(false) }
136 | expandedState.targetState = visible
137 |
138 | val transition = rememberTransition(expandedState, "Menu fade in/out")
139 |
140 | val size by transition.animateFloat(
141 | transitionSpec = {
142 | tween(MenuDefaults.animDuration)
143 | },
144 | label = "Menu fade in/out size"
145 | ) {
146 | if (it) 1F else MenuDefaults.animSizeMin
147 | }
148 |
149 | val alpha by transition.animateFloat(
150 | transitionSpec = {
151 | tween(MenuDefaults.animDuration)
152 | },
153 | label = "Menu fade in/out size"
154 | ) {
155 | if (it) 1F else 0F
156 | }
157 |
158 | Box(
159 | modifier = modifier
160 | .alpha(alpha)
161 | .graphicsLayer {
162 | scaleX = size
163 | scaleY = size
164 | }
165 | .width(IntrinsicSize.Max)
166 | .padding(MenuDefaults.margin)
167 | .shadow(
168 | elevation = MenuDefaults.elevation * alpha,
169 | shape = MenuDefaults.shape
170 | )
171 | .background(
172 | colors.background,
173 | shape = MenuDefaults.shape
174 | )
175 | .clip(
176 | shape = MenuDefaults.shape
177 | )
178 | .border(
179 | width = with(LocalDensity.current) { MenuDefaults.strokeWidthPx.toDp() },
180 | color = colors.stroke,
181 | shape = MenuDefaults.shape
182 | ),
183 | contentAlignment = Alignment.TopStart
184 | ) {
185 | Column(
186 | verticalArrangement = Arrangement.Top,
187 | horizontalAlignment = Alignment.Start
188 | ) {
189 | content(this)
190 | }
191 | }
192 | }
193 |
194 | /**
195 | * Contains the colors that define a [PopupMenu]
196 | */
197 | data class MenuColors(
198 |
199 | val background: Color,
200 |
201 | val stroke: Color
202 |
203 | )
204 |
205 | /**
206 | * Constructs the default colors for a [PopupMenu]
207 | *
208 | * @param background The color used for the background of the menu
209 | * @param stroke The color used to outline the popup
210 | * @return The [MenuColors]
211 | */
212 | @Composable
213 | fun menuColors(
214 | background: Color = MaterialTheme.colorScheme.surface,
215 | stroke: Color = MaterialTheme.colorScheme.primary
216 | ): MenuColors = MenuColors(
217 | background = background,
218 | stroke = stroke
219 | )
220 |
221 | /**
222 | * Contains default values for a [PopupMenu]
223 | */
224 | object MenuDefaults {
225 |
226 | val shape = RoundedCornerShape(
227 | 25
228 | )
229 |
230 | val elevation = 4.dp
231 |
232 | val margin = 16.dp
233 |
234 | const val animDuration = 500
235 |
236 | const val animSizeMin = 0.75F
237 |
238 | const val strokeWidthPx = 1
239 |
240 | }
241 |
242 |
243 |
244 |
245 |
246 | @Composable
247 | fun SelectableMenuItem(
248 | modifier: Modifier = Modifier,
249 | colors: MenuItemColors = menuItemColors(),
250 | onSelect: (() -> Unit)? = null,
251 | enabled: Boolean = true,
252 | label: String,
253 | selected: Boolean = false,
254 | labelStyle: androidx.compose.ui.text.TextStyle = MaterialTheme.typography.bodyMedium,
255 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
256 | ) {
257 | Row(
258 | modifier = modifier
259 | .fillMaxWidth()
260 | .clickable(
261 | interactionSource = interactionSource,
262 | indication = ripple(
263 | color = colors.ripple
264 | ),
265 | role = Role.DropdownList,
266 | onClick = { onSelect?.let { it() } },
267 | enabled = enabled
268 | )
269 | .padding(MenuItemDefaults.padding),
270 | verticalAlignment = Alignment.CenterVertically,
271 | horizontalArrangement = Arrangement.SpaceBetween
272 | ) {
273 | CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.primary) {
274 | Text(
275 | text = label,
276 | style = labelStyle,
277 | color = MaterialTheme.colorScheme.primary
278 | )
279 | Spacer(
280 | modifier = Modifier
281 | .width(MenuItemDefaults.iconSpacing)
282 | )
283 | }
284 | }
285 | }
286 |
287 |
288 | /**
289 | * Composable for a oneui-style menu item, to be used in combination with a [PopupMenu] in a menu.
290 | *
291 | * TODO: Add support for start-icons
292 | *
293 | * @param modifier The modifier to apply
294 | * @param label The string-label
295 | * @param onClick The callback for when an item is clicked
296 | * @param labelStyle The [TextStyle] of the string-label
297 | * @param interactionSource The [MutableInteractionSource]
298 | * @param colors The [MenuItemColors] to apply
299 | * @param padding The [PaddingValues] to apply
300 | */
301 | @Composable
302 | fun MenuItem(
303 | modifier: Modifier = Modifier,
304 | colors: MenuItemColors = menuItemColors(),
305 | onClick: (() -> Unit)? = null,
306 | enabled: Boolean = true,
307 | label: String,
308 | labelStyle: androidx.compose.ui.text.TextStyle = MaterialTheme.typography.bodyMedium,
309 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
310 | ) {
311 | Row(
312 | modifier = modifier
313 | .fillMaxWidth()
314 | .clickable(
315 | interactionSource = interactionSource,
316 | indication = ripple(
317 | color = colors.ripple
318 | ),
319 | role = Role.DropdownList,
320 | onClick = { onClick?.let { it() } },
321 | enabled = enabled
322 | )
323 | .padding(MenuItemDefaults.padding),
324 | verticalAlignment = Alignment.CenterVertically,
325 | horizontalArrangement = Arrangement.SpaceBetween
326 | ) {
327 | Text(
328 | text = label,
329 | style = labelStyle
330 | )
331 | }
332 | }
333 |
334 | /**
335 | * Contains the colors that define a [SelectableMenuItem]
336 | */
337 | data class MenuItemColors(
338 |
339 | val ripple: Color
340 |
341 | )
342 |
343 | /**
344 | * Constructs the default [MenuItemColors]
345 | *
346 | * @param ripple The ripple color when clicking an item
347 | * @return The [MenuItemColors]
348 | */
349 | @Composable
350 | fun menuItemColors(
351 | ripple: Color = MaterialTheme.colorScheme.onSurfaceVariant
352 | ): MenuItemColors = MenuItemColors(
353 | ripple = ripple
354 | )
355 |
356 | /**
357 | * Contains default values for a [SelectableMenuItem]
358 | */
359 | object MenuItemDefaults {
360 |
361 | val padding = PaddingValues(
362 | top = 13.dp,
363 | end = 24.dp,
364 | bottom = 13.dp,
365 | start = 24.dp
366 | )
367 |
368 | val iconSpacing = 8.dp
369 |
370 | }
--------------------------------------------------------------------------------