ios widget showing what is available at chucks
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: android app

+3811 -1
+15
.gitignore
··· 51 51 *.ipa 52 52 *.dSYM.zip 53 53 *.dSYM 54 + 55 + # Android 56 + android/.gradle/ 57 + android/build/ 58 + android/app/build/ 59 + android/local.properties 60 + android/*.apk 61 + android/*.aab 62 + android/*.jks 63 + android/*.keystore 64 + android/.idea/ 65 + android/*.iml 66 + android/captures/ 67 + android/.externalNativeBuild/ 68 + android/.cxx/
+1 -1
README.md
··· 1 1 # Wasup Chuck's 2 2 3 - ios widget showing what is available at chucks 3 + A mobile app showing what is available at Chuck's (Cedarville University dining hall). Available for iOS and Android with native widgets on both platforms. 4 4 5 5 The canonical repo for this is hosted on tangled over at [`dunkirk.sh/wasup-chucks`](https://tangled.org/@dunkirk.sh/wasup-chucks) 6 6
+79
android/.gitignore
··· 1 + # Built application files 2 + *.apk 3 + *.aar 4 + *.ap_ 5 + *.aab 6 + 7 + # Files for the ART/Dalvik VM 8 + *.dex 9 + 10 + # Java class files 11 + *.class 12 + 13 + # Generated files 14 + bin/ 15 + gen/ 16 + out/ 17 + release/ 18 + 19 + # Gradle files 20 + .gradle/ 21 + build/ 22 + 23 + # Local configuration file (sdk path, etc) 24 + local.properties 25 + 26 + # Proguard folder generated by Eclipse 27 + proguard/ 28 + 29 + # Log Files 30 + *.log 31 + 32 + # Android Studio Navigation editor temp files 33 + .navigation/ 34 + 35 + # Android Studio captures folder 36 + captures/ 37 + 38 + # IntelliJ 39 + *.iml 40 + .idea/ 41 + 42 + # Keystore files 43 + *.jks 44 + *.keystore 45 + 46 + # External native build folder generated in Android Studio 2.2 and later 47 + .externalNativeBuild 48 + .cxx/ 49 + 50 + # Google Services (e.g. APIs or Firebase) 51 + google-services.json 52 + 53 + # Freeline 54 + freeline.py 55 + freeline/ 56 + freeline_project_description.json 57 + 58 + # fastlane 59 + fastlane/report.xml 60 + fastlane/Preview.html 61 + fastlane/screenshots 62 + fastlane/test_output 63 + fastlane/readme.md 64 + 65 + # Version control 66 + vcs.xml 67 + 68 + # lint 69 + lint/intermediates/ 70 + lint/generated/ 71 + lint/outputs/ 72 + lint/tmp/ 73 + 74 + # Gradle wrapper 75 + !gradle/wrapper/gradle-wrapper.jar 76 + 77 + # OS 78 + .DS_Store 79 + Thumbs.db
+93
android/app/build.gradle.kts
··· 1 + plugins { 2 + alias(libs.plugins.android.application) 3 + alias(libs.plugins.kotlin.android) 4 + alias(libs.plugins.kotlin.compose) 5 + alias(libs.plugins.hilt.android) 6 + alias(libs.plugins.ksp) 7 + } 8 + 9 + android { 10 + namespace = "com.wasupchucks" 11 + compileSdk = 35 12 + 13 + defaultConfig { 14 + applicationId = "com.wasupchucks" 15 + minSdk = 26 16 + targetSdk = 35 17 + versionCode = 1 18 + versionName = "1.0" 19 + 20 + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 21 + } 22 + 23 + buildTypes { 24 + release { 25 + isMinifyEnabled = true 26 + isShrinkResources = true 27 + proguardFiles( 28 + getDefaultProguardFile("proguard-android-optimize.txt"), 29 + "proguard-rules.pro" 30 + ) 31 + } 32 + } 33 + compileOptions { 34 + sourceCompatibility = JavaVersion.VERSION_17 35 + targetCompatibility = JavaVersion.VERSION_17 36 + } 37 + kotlinOptions { 38 + jvmTarget = "17" 39 + } 40 + buildFeatures { 41 + compose = true 42 + } 43 + } 44 + 45 + dependencies { 46 + // Core 47 + implementation(libs.androidx.core.ktx) 48 + implementation(libs.androidx.lifecycle.runtime.ktx) 49 + implementation(libs.androidx.lifecycle.viewmodel.compose) 50 + implementation(libs.androidx.lifecycle.runtime.compose) 51 + implementation(libs.androidx.activity.compose) 52 + implementation(libs.androidx.splashscreen) 53 + 54 + // Compose 55 + implementation(platform(libs.androidx.compose.bom)) 56 + implementation(libs.androidx.ui) 57 + implementation(libs.androidx.ui.graphics) 58 + implementation(libs.androidx.ui.tooling.preview) 59 + implementation(libs.androidx.material3) 60 + implementation(libs.androidx.material.icons.extended) 61 + implementation(libs.androidx.navigation.compose) 62 + implementation(libs.androidx.material3.window.size.class) 63 + debugImplementation(libs.androidx.ui.tooling) 64 + 65 + // Hilt 66 + implementation(libs.hilt.android) 67 + ksp(libs.hilt.android.compiler) 68 + implementation(libs.hilt.navigation.compose) 69 + implementation(libs.hilt.work) 70 + ksp(libs.hilt.compiler) 71 + 72 + // Networking 73 + implementation(libs.retrofit) 74 + implementation(libs.retrofit.converter.moshi) 75 + implementation(libs.moshi) 76 + ksp(libs.moshi.kotlin.codegen) 77 + implementation(libs.okhttp) 78 + implementation(libs.okhttp.logging) 79 + 80 + // Glance widgets 81 + implementation(libs.androidx.glance) 82 + implementation(libs.androidx.glance.appwidget) 83 + implementation(libs.androidx.glance.material3) 84 + 85 + // WorkManager 86 + implementation(libs.androidx.work.runtime.ktx) 87 + 88 + // DataStore 89 + implementation(libs.androidx.datastore.preferences) 90 + 91 + // Coroutines 92 + implementation(libs.kotlinx.coroutines.android) 93 + }
+17
android/app/proguard-rules.pro
··· 1 + # Add project specific ProGuard rules here. 2 + 3 + # Keep Moshi annotations 4 + -keep class com.squareup.moshi.** { *; } 5 + -keep @com.squareup.moshi.JsonQualifier interface * 6 + -keepclassmembers @com.squareup.moshi.JsonClass class * { *; } 7 + 8 + # Keep data classes 9 + -keep class com.wasupchucks.data.model.** { *; } 10 + 11 + # OkHttp 12 + -dontwarn okhttp3.** 13 + -dontwarn okio.** 14 + 15 + # Retrofit 16 + -dontwarn retrofit2.** 17 + -keep class retrofit2.** { *; }
+78
android/app/src/main/AndroidManifest.xml
··· 1 + <?xml version="1.0" encoding="utf-8"?> 2 + <manifest xmlns:android="http://schemas.android.com/apk/res/android" 3 + xmlns:tools="http://schemas.android.com/tools"> 4 + 5 + <uses-permission android:name="android.permission.INTERNET" /> 6 + 7 + <application 8 + android:name=".WasupChucksApplication" 9 + android:allowBackup="true" 10 + android:icon="@mipmap/ic_launcher" 11 + android:label="@string/app_name" 12 + android:roundIcon="@mipmap/ic_launcher_round" 13 + android:supportsRtl="true" 14 + android:theme="@style/Theme.WasupChucks" 15 + tools:targetApi="35"> 16 + 17 + <activity 18 + android:name=".MainActivity" 19 + android:exported="true" 20 + android:theme="@style/Theme.WasupChucks"> 21 + <intent-filter> 22 + <action android:name="android.intent.action.MAIN" /> 23 + <category android:name="android.intent.category.LAUNCHER" /> 24 + </intent-filter> 25 + </activity> 26 + 27 + <!-- Widgets --> 28 + <receiver 29 + android:name=".widget.ChucksWidgetReceiver" 30 + android:exported="true" 31 + android:label="@string/widget_name_small"> 32 + <intent-filter> 33 + <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> 34 + </intent-filter> 35 + <meta-data 36 + android:name="android.appwidget.provider" 37 + android:resource="@xml/chucks_widget_info_small" /> 38 + </receiver> 39 + 40 + <receiver 41 + android:name=".widget.ChucksMediumWidgetReceiver" 42 + android:exported="true" 43 + android:label="@string/widget_name_medium"> 44 + <intent-filter> 45 + <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> 46 + </intent-filter> 47 + <meta-data 48 + android:name="android.appwidget.provider" 49 + android:resource="@xml/chucks_widget_info_medium" /> 50 + </receiver> 51 + 52 + <receiver 53 + android:name=".widget.ChucksLargeWidgetReceiver" 54 + android:exported="true" 55 + android:label="@string/widget_name_large"> 56 + <intent-filter> 57 + <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> 58 + </intent-filter> 59 + <meta-data 60 + android:name="android.appwidget.provider" 61 + android:resource="@xml/chucks_widget_info_large" /> 62 + </receiver> 63 + 64 + <!-- WorkManager initialization --> 65 + <provider 66 + android:name="androidx.startup.InitializationProvider" 67 + android:authorities="${applicationId}.androidx-startup" 68 + android:exported="false" 69 + tools:node="merge"> 70 + <meta-data 71 + android:name="androidx.work.WorkManagerInitializer" 72 + android:value="androidx.startup" 73 + tools:node="remove" /> 74 + </provider> 75 + 76 + </application> 77 + 78 + </manifest>
+27
android/app/src/main/java/com/wasupchucks/MainActivity.kt
··· 1 + package com.wasupchucks 2 + 3 + import android.os.Bundle 4 + import androidx.activity.ComponentActivity 5 + import androidx.activity.compose.setContent 6 + import androidx.activity.enableEdgeToEdge 7 + import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi 8 + import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass 9 + import com.wasupchucks.ui.screens.home.HomeScreen 10 + import com.wasupchucks.ui.theme.WasupChucksTheme 11 + import dagger.hilt.android.AndroidEntryPoint 12 + 13 + @AndroidEntryPoint 14 + class MainActivity : ComponentActivity() { 15 + @OptIn(ExperimentalMaterial3WindowSizeClassApi::class) 16 + override fun onCreate(savedInstanceState: Bundle?) { 17 + super.onCreate(savedInstanceState) 18 + enableEdgeToEdge() 19 + 20 + setContent { 21 + WasupChucksTheme { 22 + val windowSizeClass = calculateWindowSizeClass(this) 23 + HomeScreen(widthSizeClass = windowSizeClass.widthSizeClass) 24 + } 25 + } 26 + } 27 + }
+26
android/app/src/main/java/com/wasupchucks/WasupChucksApplication.kt
··· 1 + package com.wasupchucks 2 + 3 + import android.app.Application 4 + import androidx.hilt.work.HiltWorkerFactory 5 + import androidx.work.Configuration 6 + import com.wasupchucks.widget.WidgetRefreshWorker 7 + import dagger.hilt.android.HiltAndroidApp 8 + import javax.inject.Inject 9 + 10 + @HiltAndroidApp 11 + class WasupChucksApplication : Application(), Configuration.Provider { 12 + 13 + @Inject 14 + lateinit var workerFactory: HiltWorkerFactory 15 + 16 + override val workManagerConfiguration: Configuration 17 + get() = Configuration.Builder() 18 + .setWorkerFactory(workerFactory) 19 + .build() 20 + 21 + override fun onCreate() { 22 + super.onCreate() 23 + // Start periodic widget updates 24 + WidgetRefreshWorker.enqueue(this) 25 + } 26 + }
+19
android/app/src/main/java/com/wasupchucks/data/api/ChucksApiInterceptor.kt
··· 1 + package com.wasupchucks.data.api 2 + 3 + import okhttp3.Interceptor 4 + import okhttp3.Response 5 + import javax.inject.Inject 6 + 7 + class ChucksApiInterceptor @Inject constructor() : Interceptor { 8 + override fun intercept(chain: Interceptor.Chain): Response { 9 + val originalRequest = chain.request() 10 + 11 + val modifiedRequest = originalRequest.newBuilder() 12 + .header("Accept", "*/*") 13 + .header("Origin", "https://www.cedarville.edu") 14 + .header("Referer", "https://www.cedarville.edu/offices/the-commons") 15 + .build() 16 + 17 + return chain.proceed(modifiedRequest) 18 + } 19 + }
+12
android/app/src/main/java/com/wasupchucks/data/api/ChucksApiService.kt
··· 1 + package com.wasupchucks.data.api 2 + 3 + import com.wasupchucks.data.model.MenuResponse 4 + import retrofit2.http.GET 5 + import retrofit2.http.Query 6 + 7 + interface ChucksApiService { 8 + @GET("menus") 9 + suspend fun fetchMenu( 10 + @Query("days") days: Int = 5 11 + ): MenuResponse 12 + }
+42
android/app/src/main/java/com/wasupchucks/data/model/Allergen.kt
··· 1 + package com.wasupchucks.data.model 2 + 3 + import com.squareup.moshi.JsonClass 4 + 5 + @JsonClass(generateAdapter = true) 6 + data class Allergen( 7 + val url: String, 8 + val alt: String 9 + ) { 10 + val symbol: String 11 + get() = when (alt) { 12 + "gluten" -> "G" 13 + "dairy" -> "D" 14 + "egg" -> "E" 15 + "soy" -> "S" 16 + "fish" -> "F" 17 + "hasPeanut" -> "P" 18 + "tree nut" -> "N" 19 + "hasShellfish" -> "SF" 20 + "vegetarian" -> "V" 21 + "gluten-free" -> "GF" 22 + else -> "?" 23 + } 24 + 25 + val displayName: String 26 + get() = when (alt) { 27 + "gluten" -> "gluten" 28 + "dairy" -> "dairy" 29 + "egg" -> "egg" 30 + "soy" -> "soy" 31 + "fish" -> "fish" 32 + "hasPeanut" -> "peanuts" 33 + "tree nut" -> "tree nuts" 34 + "hasShellfish" -> "shellfish" 35 + "vegetarian" -> "vegetarian" 36 + "gluten-free" -> "gluten-free" 37 + else -> alt 38 + } 39 + 40 + val isDietary: Boolean 41 + get() = alt == "vegetarian" || alt == "gluten-free" 42 + }
+144
android/app/src/main/java/com/wasupchucks/data/model/ChucksStatus.kt
··· 1 + package com.wasupchucks.data.model 2 + 3 + import java.time.LocalDate 4 + import java.time.LocalDateTime 5 + import java.time.ZoneId 6 + import java.time.ZonedDateTime 7 + import java.time.temporal.ChronoUnit 8 + import kotlin.time.Duration 9 + import kotlin.time.Duration.Companion.seconds 10 + 11 + data class ChucksStatus( 12 + val currentPhase: MealPhase, 13 + val timeRemaining: Duration?, 14 + val nextPhase: MealPhase?, 15 + val nextPhaseStart: ZonedDateTime?, 16 + val isOpen: Boolean, 17 + val currentMealEnd: ZonedDateTime? 18 + ) { 19 + companion object { 20 + private val cedarvilleZone = ZoneId.of("America/New_York") 21 + 22 + fun calculate(dateTime: ZonedDateTime = ZonedDateTime.now(cedarvilleZone)): ChucksStatus { 23 + val localDateTime = dateTime.withZoneSameInstant(cedarvilleZone).toLocalDateTime() 24 + val schedule = MealSchedule.scheduleFor(localDateTime.dayOfWeek) 25 + 26 + val currentMinutes = localDateTime.hour * 60 + localDateTime.minute 27 + 28 + // Check each meal period 29 + for ((index, meal) in schedule.withIndex()) { 30 + // Currently in a meal period 31 + if (currentMinutes >= meal.startMinutes && currentMinutes < meal.endMinutes) { 32 + val endDateTime = localDateTime.toLocalDate() 33 + .atTime(meal.endHour, meal.endMinute) 34 + .atZone(cedarvilleZone) 35 + val remaining = ChronoUnit.SECONDS.between(dateTime, endDateTime).seconds 36 + 37 + val nextPhase: MealPhase? 38 + val nextStart: ZonedDateTime? 39 + if (index + 1 < schedule.size) { 40 + val next = schedule[index + 1] 41 + nextPhase = next.phase 42 + nextStart = localDateTime.toLocalDate() 43 + .atTime(next.startHour, next.startMinute) 44 + .atZone(cedarvilleZone) 45 + } else { 46 + nextPhase = MealPhase.CLOSED 47 + nextStart = null 48 + } 49 + 50 + return ChucksStatus( 51 + currentPhase = meal.phase, 52 + timeRemaining = remaining, 53 + nextPhase = nextPhase, 54 + nextPhaseStart = nextStart, 55 + isOpen = true, 56 + currentMealEnd = endDateTime 57 + ) 58 + } 59 + 60 + // Before a meal starts 61 + if (currentMinutes < meal.startMinutes) { 62 + val startDateTime = localDateTime.toLocalDate() 63 + .atTime(meal.startHour, meal.startMinute) 64 + .atZone(cedarvilleZone) 65 + val timeUntil = ChronoUnit.SECONDS.between(dateTime, startDateTime).seconds 66 + 67 + return ChucksStatus( 68 + currentPhase = MealPhase.CLOSED, 69 + timeRemaining = timeUntil, 70 + nextPhase = meal.phase, 71 + nextPhaseStart = startDateTime, 72 + isOpen = false, 73 + currentMealEnd = null 74 + ) 75 + } 76 + } 77 + 78 + // After all meals - calculate time until tomorrow's first meal 79 + val tomorrow = localDateTime.toLocalDate().plusDays(1) 80 + val tomorrowSchedule = MealSchedule.scheduleFor(tomorrow.dayOfWeek) 81 + 82 + if (tomorrowSchedule.isNotEmpty()) { 83 + val firstMeal = tomorrowSchedule.first() 84 + var nextStart = tomorrow 85 + .atTime(firstMeal.startHour, firstMeal.startMinute) 86 + .atZone(cedarvilleZone) 87 + 88 + // Handle edge case if calculated time is in the past 89 + if (!nextStart.isAfter(dateTime)) { 90 + nextStart = nextStart.plusDays(1) 91 + } 92 + 93 + val timeUntil = ChronoUnit.SECONDS.between(dateTime, nextStart).seconds 94 + 95 + return ChucksStatus( 96 + currentPhase = MealPhase.CLOSED, 97 + timeRemaining = timeUntil, 98 + nextPhase = firstMeal.phase, 99 + nextPhaseStart = nextStart, 100 + isOpen = false, 101 + currentMealEnd = null 102 + ) 103 + } 104 + 105 + // Fallback - should never reach here 106 + return ChucksStatus( 107 + currentPhase = MealPhase.CLOSED, 108 + timeRemaining = null, 109 + nextPhase = null, 110 + nextPhaseStart = null, 111 + isOpen = false, 112 + currentMealEnd = null 113 + ) 114 + } 115 + } 116 + } 117 + 118 + // Extension functions for Duration formatting 119 + fun Duration.toCompactCountdown(): String { 120 + val totalSeconds = inWholeSeconds 121 + val hours = totalSeconds / 3600 122 + val minutes = (totalSeconds % 3600) / 60 123 + val seconds = totalSeconds % 60 124 + 125 + return when { 126 + hours > 0 -> "${hours}h" 127 + minutes > 0 -> "${minutes}m" 128 + else -> "${seconds}s" 129 + } 130 + } 131 + 132 + fun Duration.toExpandedCountdown(): String { 133 + val totalSeconds = inWholeSeconds 134 + val hours = totalSeconds / 3600 135 + val minutes = (totalSeconds % 3600) / 60 136 + val seconds = totalSeconds % 60 137 + 138 + return when { 139 + hours > 0 && minutes > 0 -> "${hours}h ${minutes}m" 140 + hours > 0 -> "${hours}h" 141 + minutes > 0 -> "${minutes}m" 142 + else -> "${seconds}s" 143 + } 144 + }
+26
android/app/src/main/java/com/wasupchucks/data/model/MealPhase.kt
··· 1 + package com.wasupchucks.data.model 2 + 3 + import androidx.compose.material.icons.Icons 4 + import androidx.compose.material.icons.filled.Bedtime 5 + import androidx.compose.material.icons.filled.DinnerDining 6 + import androidx.compose.material.icons.filled.FreeBreakfast 7 + import androidx.compose.material.icons.filled.LunchDining 8 + import androidx.compose.ui.graphics.vector.ImageVector 9 + 10 + enum class MealPhase( 11 + val displayName: String, 12 + val apiSlot: String 13 + ) { 14 + BREAKFAST("Breakfast", "breakfast"), 15 + LUNCH("Lunch", "lunch"), 16 + DINNER("Dinner", "dinner"), 17 + CLOSED("Closed", ""); 18 + 19 + val icon: ImageVector 20 + get() = when (this) { 21 + BREAKFAST -> Icons.Filled.FreeBreakfast 22 + LUNCH -> Icons.Filled.LunchDining 23 + DINNER -> Icons.Filled.DinnerDining 24 + CLOSED -> Icons.Filled.Bedtime 25 + } 26 + }
+57
android/app/src/main/java/com/wasupchucks/data/model/MealSchedule.kt
··· 1 + package com.wasupchucks.data.model 2 + 3 + import java.time.DayOfWeek 4 + import java.time.LocalDate 5 + import java.time.ZoneId 6 + 7 + data class MealSchedule( 8 + val phase: MealPhase, 9 + val startHour: Int, 10 + val startMinute: Int, 11 + val endHour: Int, 12 + val endMinute: Int 13 + ) { 14 + val startMinutes: Int 15 + get() = startHour * 60 + startMinute 16 + 17 + val endMinutes: Int 18 + get() = endHour * 60 + endMinute 19 + 20 + companion object { 21 + // Mon-Fri: Hot Breakfast 7-8:15, Continental 8:15-9:30, Lunch 10:30-2:30, Dinner 4:30-7:30 22 + // Treating Hot + Continental as one "Breakfast" period for simplicity 23 + val weekdaySchedule = listOf( 24 + MealSchedule(MealPhase.BREAKFAST, 7, 0, 9, 30), 25 + MealSchedule(MealPhase.LUNCH, 10, 30, 14, 30), 26 + MealSchedule(MealPhase.DINNER, 16, 30, 19, 30) 27 + ) 28 + 29 + // Saturday: Continental 8-9, Lunch 11-1, Dinner 4:30-6:30 30 + val saturdaySchedule = listOf( 31 + MealSchedule(MealPhase.BREAKFAST, 8, 0, 9, 0), 32 + MealSchedule(MealPhase.LUNCH, 11, 0, 13, 0), 33 + MealSchedule(MealPhase.DINNER, 16, 30, 18, 30) 34 + ) 35 + 36 + // Sunday: Hot Breakfast 8-9, Lunch 11:30-2, Dinner 5-7:30 37 + val sundaySchedule = listOf( 38 + MealSchedule(MealPhase.BREAKFAST, 8, 0, 9, 0), 39 + MealSchedule(MealPhase.LUNCH, 11, 30, 14, 0), 40 + MealSchedule(MealPhase.DINNER, 17, 0, 19, 30) 41 + ) 42 + 43 + fun scheduleFor(dayOfWeek: DayOfWeek): List<MealSchedule> { 44 + return when (dayOfWeek) { 45 + DayOfWeek.SUNDAY -> sundaySchedule 46 + DayOfWeek.SATURDAY -> saturdaySchedule 47 + else -> weekdaySchedule 48 + } 49 + } 50 + 51 + fun scheduleForToday(): List<MealSchedule> { 52 + val cedarvilleZone = ZoneId.of("America/New_York") 53 + val today = LocalDate.now(cedarvilleZone) 54 + return scheduleFor(today.dayOfWeek) 55 + } 56 + } 57 + }
+12
android/app/src/main/java/com/wasupchucks/data/model/MenuItem.kt
··· 1 + package com.wasupchucks.data.model 2 + 3 + import com.squareup.moshi.JsonClass 4 + 5 + @JsonClass(generateAdapter = true) 6 + data class MenuItem( 7 + val name: String, 8 + val allergens: List<Allergen> 9 + ) { 10 + val id: String 11 + get() = "$name-${allergens.joinToString("") { it.alt }}" 12 + }
+16
android/app/src/main/java/com/wasupchucks/data/model/VenueMenu.kt
··· 1 + package com.wasupchucks.data.model 2 + 3 + import com.squareup.moshi.JsonClass 4 + 5 + @JsonClass(generateAdapter = true) 6 + data class VenueMenu( 7 + val venue: String, 8 + val meal: String?, 9 + val slot: String, 10 + val items: List<MenuItem> 11 + ) { 12 + val id: String 13 + get() = "$venue-$slot-${meal ?: ""}" 14 + } 15 + 16 + typealias MenuResponse = Map<String, List<VenueMenu>>
+24
android/app/src/main/java/com/wasupchucks/data/repository/MenuRepository.kt
··· 1 + package com.wasupchucks.data.repository 2 + 3 + import com.wasupchucks.data.model.MealPhase 4 + import com.wasupchucks.data.model.MenuItem 5 + import com.wasupchucks.data.model.VenueMenu 6 + import java.time.LocalDate 7 + 8 + interface MenuRepository { 9 + suspend fun fetchMenu(): Result<Map<String, List<VenueMenu>>> 10 + suspend fun getMenuForDate(date: LocalDate): Result<List<VenueMenu>> 11 + suspend fun getSpecials(date: LocalDate, phase: MealPhase): Result<List<MenuItem>> 12 + suspend fun getSpecialsWithVenue(date: LocalDate, phase: MealPhase): Result<Pair<List<MenuItem>, String>> 13 + fun invalidateCache() 14 + } 15 + 16 + sealed class ChucksError : Exception() { 17 + data object InvalidUrl : ChucksError() { 18 + private fun readResolve(): Any = InvalidUrl 19 + } 20 + data object NetworkError : ChucksError() { 21 + private fun readResolve(): Any = NetworkError 22 + } 23 + data class DecodingError(override val cause: Throwable) : ChucksError() 24 + }
+85
android/app/src/main/java/com/wasupchucks/data/repository/MenuRepositoryImpl.kt
··· 1 + package com.wasupchucks.data.repository 2 + 3 + import com.wasupchucks.data.api.ChucksApiService 4 + import com.wasupchucks.data.model.MealPhase 5 + import com.wasupchucks.data.model.MenuItem 6 + import com.wasupchucks.data.model.VenueMenu 7 + import kotlinx.coroutines.sync.Mutex 8 + import kotlinx.coroutines.sync.withLock 9 + import java.time.LocalDate 10 + import java.time.ZoneId 11 + import java.time.format.DateTimeFormatter 12 + import javax.inject.Inject 13 + import javax.inject.Singleton 14 + 15 + @Singleton 16 + class MenuRepositoryImpl @Inject constructor( 17 + private val apiService: ChucksApiService 18 + ) : MenuRepository { 19 + 20 + private val mutex = Mutex() 21 + private var cachedMenu: Map<String, List<VenueMenu>>? = null 22 + private var cacheTime: Long = 0 23 + private val cacheExpiration = 60 * 60 * 1000L // 1 hour in milliseconds 24 + 25 + private val dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") 26 + .withZone(ZoneId.of("America/New_York")) 27 + 28 + override suspend fun fetchMenu(): Result<Map<String, List<VenueMenu>>> { 29 + return mutex.withLock { 30 + // Check cache 31 + val currentTime = System.currentTimeMillis() 32 + val cached = cachedMenu 33 + if (cached != null && currentTime - cacheTime < cacheExpiration) { 34 + return@withLock Result.success(cached) 35 + } 36 + 37 + // Fetch from API 38 + try { 39 + val menu = apiService.fetchMenu() 40 + cachedMenu = menu 41 + cacheTime = currentTime 42 + Result.success(menu) 43 + } catch (e: retrofit2.HttpException) { 44 + Result.failure(ChucksError.NetworkError) 45 + } catch (e: java.io.IOException) { 46 + Result.failure(ChucksError.NetworkError) 47 + } catch (e: com.squareup.moshi.JsonDataException) { 48 + Result.failure(ChucksError.DecodingError(e)) 49 + } catch (e: Exception) { 50 + Result.failure(ChucksError.DecodingError(e)) 51 + } 52 + } 53 + } 54 + 55 + override suspend fun getMenuForDate(date: LocalDate): Result<List<VenueMenu>> { 56 + return fetchMenu().map { menu -> 57 + val dateKey = date.format(dateFormatter) 58 + menu[dateKey] ?: emptyList() 59 + } 60 + } 61 + 62 + override suspend fun getSpecials(date: LocalDate, phase: MealPhase): Result<List<MenuItem>> { 63 + return getMenuForDate(date).map { dayMenu -> 64 + val slot = phase.apiSlot 65 + dayMenu 66 + .filter { it.venue == "Home Cooking" && it.slot == slot } 67 + .flatMap { it.items } 68 + } 69 + } 70 + 71 + override suspend fun getSpecialsWithVenue( 72 + date: LocalDate, 73 + phase: MealPhase 74 + ): Result<Pair<List<MenuItem>, String>> { 75 + val venueName = "Home Cooking" 76 + return getSpecials(date, phase).map { items -> 77 + items to venueName 78 + } 79 + } 80 + 81 + override fun invalidateCache() { 82 + cachedMenu = null 83 + cacheTime = 0 84 + } 85 + }
+41
android/app/src/main/java/com/wasupchucks/di/AppModule.kt
··· 1 + package com.wasupchucks.di 2 + 3 + import android.content.Context 4 + import androidx.datastore.core.DataStore 5 + import androidx.datastore.preferences.core.Preferences 6 + import androidx.datastore.preferences.preferencesDataStore 7 + import com.wasupchucks.data.repository.MenuRepository 8 + import com.wasupchucks.data.repository.MenuRepositoryImpl 9 + import dagger.Binds 10 + import dagger.Module 11 + import dagger.Provides 12 + import dagger.hilt.InstallIn 13 + import dagger.hilt.android.qualifiers.ApplicationContext 14 + import dagger.hilt.components.SingletonComponent 15 + import javax.inject.Singleton 16 + 17 + private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "widget_prefs") 18 + 19 + @Module 20 + @InstallIn(SingletonComponent::class) 21 + abstract class AppModule { 22 + 23 + @Binds 24 + @Singleton 25 + abstract fun bindMenuRepository( 26 + menuRepositoryImpl: MenuRepositoryImpl 27 + ): MenuRepository 28 + } 29 + 30 + @Module 31 + @InstallIn(SingletonComponent::class) 32 + object DataStoreModule { 33 + 34 + @Provides 35 + @Singleton 36 + fun provideDataStore( 37 + @ApplicationContext context: Context 38 + ): DataStore<Preferences> { 39 + return context.dataStore 40 + } 41 + }
+65
android/app/src/main/java/com/wasupchucks/di/NetworkModule.kt
··· 1 + package com.wasupchucks.di 2 + 3 + import com.squareup.moshi.Moshi 4 + import com.wasupchucks.data.api.ChucksApiInterceptor 5 + import com.wasupchucks.data.api.ChucksApiService 6 + import dagger.Module 7 + import dagger.Provides 8 + import dagger.hilt.InstallIn 9 + import dagger.hilt.components.SingletonComponent 10 + import okhttp3.OkHttpClient 11 + import okhttp3.logging.HttpLoggingInterceptor 12 + import retrofit2.Retrofit 13 + import retrofit2.converter.moshi.MoshiConverterFactory 14 + import java.util.concurrent.TimeUnit 15 + import javax.inject.Singleton 16 + 17 + @Module 18 + @InstallIn(SingletonComponent::class) 19 + object NetworkModule { 20 + 21 + private const val BASE_URL = "https://diningdata.cedarville.edu/api/" 22 + private const val TIMEOUT_SECONDS = 30L 23 + 24 + @Provides 25 + @Singleton 26 + fun provideMoshi(): Moshi { 27 + return Moshi.Builder() 28 + .build() 29 + } 30 + 31 + @Provides 32 + @Singleton 33 + fun provideOkHttpClient( 34 + chucksApiInterceptor: ChucksApiInterceptor 35 + ): OkHttpClient { 36 + return OkHttpClient.Builder() 37 + .addInterceptor(chucksApiInterceptor) 38 + .addInterceptor(HttpLoggingInterceptor().apply { 39 + level = HttpLoggingInterceptor.Level.BASIC 40 + }) 41 + .connectTimeout(TIMEOUT_SECONDS, TimeUnit.SECONDS) 42 + .readTimeout(TIMEOUT_SECONDS, TimeUnit.SECONDS) 43 + .writeTimeout(TIMEOUT_SECONDS, TimeUnit.SECONDS) 44 + .build() 45 + } 46 + 47 + @Provides 48 + @Singleton 49 + fun provideRetrofit( 50 + okHttpClient: OkHttpClient, 51 + moshi: Moshi 52 + ): Retrofit { 53 + return Retrofit.Builder() 54 + .baseUrl(BASE_URL) 55 + .client(okHttpClient) 56 + .addConverterFactory(MoshiConverterFactory.create(moshi)) 57 + .build() 58 + } 59 + 60 + @Provides 61 + @Singleton 62 + fun provideChucksApiService(retrofit: Retrofit): ChucksApiService { 63 + return retrofit.create(ChucksApiService::class.java) 64 + } 65 + }
+67
android/app/src/main/java/com/wasupchucks/ui/components/AllergenBadge.kt
··· 1 + package com.wasupchucks.ui.components 2 + 3 + import androidx.compose.foundation.layout.Arrangement 4 + import androidx.compose.foundation.layout.Row 5 + import androidx.compose.foundation.layout.padding 6 + import androidx.compose.foundation.shape.RoundedCornerShape 7 + import androidx.compose.material3.Surface 8 + import androidx.compose.material3.Text 9 + import androidx.compose.runtime.Composable 10 + import androidx.compose.ui.Modifier 11 + import androidx.compose.ui.res.stringResource 12 + import androidx.compose.ui.semantics.contentDescription 13 + import androidx.compose.ui.semantics.semantics 14 + import androidx.compose.ui.text.font.FontWeight 15 + import androidx.compose.ui.unit.dp 16 + import androidx.compose.ui.unit.sp 17 + import com.wasupchucks.R 18 + import com.wasupchucks.data.model.Allergen 19 + import com.wasupchucks.ui.theme.AllergenDietary 20 + import com.wasupchucks.ui.theme.AllergenDietaryContainer 21 + import com.wasupchucks.ui.theme.AllergenWarning 22 + import com.wasupchucks.ui.theme.AllergenWarningContainer 23 + 24 + @Composable 25 + fun AllergenBadge( 26 + allergen: Allergen, 27 + modifier: Modifier = Modifier 28 + ) { 29 + val backgroundColor = if (allergen.isDietary) AllergenDietaryContainer else AllergenWarningContainer 30 + val textColor = if (allergen.isDietary) AllergenDietary else AllergenWarning 31 + 32 + Surface( 33 + modifier = modifier, 34 + shape = RoundedCornerShape(4.dp), 35 + color = backgroundColor 36 + ) { 37 + Text( 38 + text = allergen.symbol, 39 + fontSize = 9.sp, 40 + fontWeight = FontWeight.Bold, 41 + color = textColor, 42 + modifier = Modifier.padding(horizontal = 5.dp, vertical = 3.dp) 43 + ) 44 + } 45 + } 46 + 47 + @Composable 48 + fun AllergenRow( 49 + allergens: List<Allergen>, 50 + modifier: Modifier = Modifier 51 + ) { 52 + if (allergens.isEmpty()) return 53 + 54 + val accessibilityLabel = stringResource( 55 + R.string.contains_allergens, 56 + allergens.joinToString(", ") { it.displayName } 57 + ) 58 + 59 + Row( 60 + modifier = modifier.semantics { contentDescription = accessibilityLabel }, 61 + horizontalArrangement = Arrangement.spacedBy(4.dp) 62 + ) { 63 + allergens.forEach { allergen -> 64 + AllergenBadge(allergen = allergen) 65 + } 66 + } 67 + }
+86
android/app/src/main/java/com/wasupchucks/ui/components/ErrorCard.kt
··· 1 + package com.wasupchucks.ui.components 2 + 3 + import androidx.compose.foundation.layout.Arrangement 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.padding 9 + import androidx.compose.foundation.layout.size 10 + import androidx.compose.material.icons.Icons 11 + import androidx.compose.material.icons.filled.WifiOff 12 + import androidx.compose.material3.Button 13 + import androidx.compose.material3.ButtonDefaults 14 + import androidx.compose.material3.ElevatedCard 15 + import androidx.compose.material3.Icon 16 + import androidx.compose.material3.MaterialTheme 17 + import androidx.compose.material3.Text 18 + import androidx.compose.runtime.Composable 19 + import androidx.compose.ui.Alignment 20 + import androidx.compose.ui.Modifier 21 + import androidx.compose.ui.res.stringResource 22 + import androidx.compose.ui.text.style.TextAlign 23 + import androidx.compose.ui.unit.dp 24 + import com.wasupchucks.R 25 + import com.wasupchucks.data.repository.ChucksError 26 + import com.wasupchucks.ui.theme.StatusClosed 27 + 28 + @Composable 29 + fun ErrorCard( 30 + error: Throwable, 31 + onRetry: () -> Unit, 32 + modifier: Modifier = Modifier 33 + ) { 34 + val errorMessage = when (error) { 35 + is ChucksError.InvalidUrl -> stringResource(R.string.error_invalid_url) 36 + is ChucksError.NetworkError -> stringResource(R.string.error_network) 37 + is ChucksError.DecodingError -> stringResource(R.string.error_decoding) 38 + else -> stringResource(R.string.error_generic) 39 + } 40 + 41 + ElevatedCard( 42 + modifier = modifier.fillMaxWidth() 43 + ) { 44 + Column( 45 + modifier = Modifier 46 + .fillMaxWidth() 47 + .padding(24.dp), 48 + horizontalAlignment = Alignment.CenterHorizontally, 49 + verticalArrangement = Arrangement.Center 50 + ) { 51 + Icon( 52 + imageVector = Icons.Filled.WifiOff, 53 + contentDescription = null, 54 + modifier = Modifier.size(40.dp), 55 + tint = MaterialTheme.colorScheme.onSurfaceVariant 56 + ) 57 + 58 + Spacer(modifier = Modifier.height(16.dp)) 59 + 60 + Text( 61 + text = stringResource(R.string.error_title), 62 + style = MaterialTheme.typography.titleMedium 63 + ) 64 + 65 + Spacer(modifier = Modifier.height(8.dp)) 66 + 67 + Text( 68 + text = errorMessage, 69 + style = MaterialTheme.typography.bodyMedium, 70 + color = MaterialTheme.colorScheme.onSurfaceVariant, 71 + textAlign = TextAlign.Center 72 + ) 73 + 74 + Spacer(modifier = Modifier.height(16.dp)) 75 + 76 + Button( 77 + onClick = onRetry, 78 + colors = ButtonDefaults.buttonColors( 79 + containerColor = StatusClosed 80 + ) 81 + ) { 82 + Text(text = stringResource(R.string.try_again)) 83 + } 84 + } 85 + } 86 + }
+152
android/app/src/main/java/com/wasupchucks/ui/components/MealDetailSheet.kt
··· 1 + package com.wasupchucks.ui.components 2 + 3 + import androidx.compose.foundation.layout.Arrangement 4 + import androidx.compose.foundation.layout.Box 5 + import androidx.compose.foundation.layout.Column 6 + import androidx.compose.foundation.layout.Row 7 + import androidx.compose.foundation.layout.Spacer 8 + import androidx.compose.foundation.layout.fillMaxSize 9 + import androidx.compose.foundation.layout.fillMaxWidth 10 + import androidx.compose.foundation.layout.height 11 + import androidx.compose.foundation.layout.padding 12 + import androidx.compose.foundation.lazy.LazyColumn 13 + import androidx.compose.foundation.lazy.items 14 + import androidx.compose.material.icons.Icons 15 + import androidx.compose.material.icons.filled.Close 16 + import androidx.compose.material.icons.filled.Restaurant 17 + import androidx.compose.material3.ExperimentalMaterial3Api 18 + import androidx.compose.material3.HorizontalDivider 19 + import androidx.compose.material3.Icon 20 + import androidx.compose.material3.IconButton 21 + import androidx.compose.material3.MaterialTheme 22 + import androidx.compose.material3.ModalBottomSheet 23 + import androidx.compose.material3.Text 24 + import androidx.compose.material3.rememberModalBottomSheetState 25 + import androidx.compose.runtime.Composable 26 + import androidx.compose.ui.Alignment 27 + import androidx.compose.ui.Modifier 28 + import androidx.compose.ui.res.stringResource 29 + import androidx.compose.ui.text.font.FontWeight 30 + import androidx.compose.ui.unit.dp 31 + import com.wasupchucks.R 32 + import com.wasupchucks.data.model.MealSchedule 33 + import com.wasupchucks.data.model.VenueMenu 34 + 35 + @OptIn(ExperimentalMaterial3Api::class) 36 + @Composable 37 + fun MealDetailSheet( 38 + meal: MealSchedule, 39 + menu: List<VenueMenu>, 40 + onDismiss: () -> Unit, 41 + modifier: Modifier = Modifier 42 + ) { 43 + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = false) 44 + 45 + val venues = menu 46 + .filter { it.slot == meal.phase.apiSlot } 47 + .sortedBy { it.venue } 48 + 49 + ModalBottomSheet( 50 + onDismissRequest = onDismiss, 51 + sheetState = sheetState, 52 + modifier = modifier 53 + ) { 54 + Column( 55 + modifier = Modifier 56 + .fillMaxWidth() 57 + .padding(horizontal = 16.dp) 58 + ) { 59 + // Header 60 + Row( 61 + modifier = Modifier.fillMaxWidth(), 62 + horizontalArrangement = Arrangement.SpaceBetween, 63 + verticalAlignment = Alignment.CenterVertically 64 + ) { 65 + Text( 66 + text = meal.phase.displayName, 67 + style = MaterialTheme.typography.headlineSmall, 68 + fontWeight = FontWeight.Bold 69 + ) 70 + IconButton(onClick = onDismiss) { 71 + Icon( 72 + imageVector = Icons.Filled.Close, 73 + contentDescription = "Close" 74 + ) 75 + } 76 + } 77 + 78 + Spacer(modifier = Modifier.height(8.dp)) 79 + 80 + if (venues.isEmpty()) { 81 + // Empty state 82 + Box( 83 + modifier = Modifier 84 + .fillMaxWidth() 85 + .height(200.dp), 86 + contentAlignment = Alignment.Center 87 + ) { 88 + Column( 89 + horizontalAlignment = Alignment.CenterHorizontally, 90 + verticalArrangement = Arrangement.spacedBy(8.dp) 91 + ) { 92 + Icon( 93 + imageVector = Icons.Filled.Restaurant, 94 + contentDescription = null, 95 + tint = MaterialTheme.colorScheme.onSurfaceVariant 96 + ) 97 + Text( 98 + text = stringResource(R.string.no_menu_available), 99 + style = MaterialTheme.typography.titleMedium 100 + ) 101 + Text( 102 + text = stringResource(R.string.no_menu_description, meal.phase.displayName), 103 + style = MaterialTheme.typography.bodyMedium, 104 + color = MaterialTheme.colorScheme.onSurfaceVariant 105 + ) 106 + } 107 + } 108 + } else { 109 + LazyColumn( 110 + modifier = Modifier.fillMaxSize(), 111 + verticalArrangement = Arrangement.spacedBy(8.dp) 112 + ) { 113 + venues.forEach { venue -> 114 + item(key = "header-${venue.id}") { 115 + if (venues.indexOf(venue) > 0) { 116 + HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp)) 117 + } 118 + Text( 119 + text = venue.venue, 120 + style = MaterialTheme.typography.titleSmall, 121 + fontWeight = FontWeight.SemiBold, 122 + color = MaterialTheme.colorScheme.primary, 123 + modifier = Modifier.padding(top = 8.dp) 124 + ) 125 + } 126 + 127 + items(venue.items, key = { "${venue.id}-${it.id}" }) { item -> 128 + Row( 129 + modifier = Modifier 130 + .fillMaxWidth() 131 + .padding(vertical = 4.dp), 132 + horizontalArrangement = Arrangement.SpaceBetween, 133 + verticalAlignment = Alignment.CenterVertically 134 + ) { 135 + Text( 136 + text = item.name, 137 + style = MaterialTheme.typography.bodyMedium, 138 + modifier = Modifier.weight(1f) 139 + ) 140 + AllergenRow(allergens = item.allergens) 141 + } 142 + } 143 + } 144 + 145 + item { 146 + Spacer(modifier = Modifier.height(32.dp)) 147 + } 148 + } 149 + } 150 + } 151 + } 152 + }
+143
android/app/src/main/java/com/wasupchucks/ui/components/ScheduleCard.kt
··· 1 + package com.wasupchucks.ui.components 2 + 3 + import androidx.compose.foundation.BorderStroke 4 + import androidx.compose.foundation.layout.Arrangement 5 + import androidx.compose.foundation.layout.Column 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.material3.ElevatedCard 12 + import androidx.compose.material3.FilledTonalButton 13 + import androidx.compose.material3.Icon 14 + import androidx.compose.material3.MaterialTheme 15 + import androidx.compose.material3.OutlinedButton 16 + import androidx.compose.material3.Text 17 + import androidx.compose.runtime.Composable 18 + import androidx.compose.ui.Alignment 19 + import androidx.compose.ui.Modifier 20 + import androidx.compose.ui.res.stringResource 21 + import androidx.compose.ui.semantics.contentDescription 22 + import androidx.compose.ui.semantics.semantics 23 + import androidx.compose.ui.text.font.FontWeight 24 + import androidx.compose.ui.unit.dp 25 + import com.wasupchucks.R 26 + import com.wasupchucks.data.model.ChucksStatus 27 + import com.wasupchucks.data.model.MealSchedule 28 + import com.wasupchucks.ui.theme.StatusOpen 29 + 30 + @Composable 31 + fun ScheduleCard( 32 + status: ChucksStatus, 33 + schedule: List<MealSchedule>, 34 + onMealClick: (MealSchedule) -> Unit, 35 + modifier: Modifier = Modifier 36 + ) { 37 + ElevatedCard( 38 + modifier = modifier.fillMaxWidth() 39 + ) { 40 + Column( 41 + modifier = Modifier 42 + .fillMaxWidth() 43 + .padding(16.dp) 44 + ) { 45 + Text( 46 + text = stringResource(R.string.todays_schedule), 47 + style = MaterialTheme.typography.titleMedium, 48 + fontWeight = FontWeight.SemiBold 49 + ) 50 + 51 + Spacer(modifier = Modifier.height(12.dp)) 52 + 53 + Row( 54 + modifier = Modifier.fillMaxWidth(), 55 + horizontalArrangement = Arrangement.spacedBy(8.dp) 56 + ) { 57 + schedule.forEach { meal -> 58 + val isCurrent = status.isOpen && status.currentPhase == meal.phase 59 + 60 + ScheduleButton( 61 + meal = meal, 62 + isCurrent = isCurrent, 63 + onClick = { onMealClick(meal) }, 64 + modifier = Modifier.weight(1f) 65 + ) 66 + } 67 + } 68 + } 69 + } 70 + } 71 + 72 + @Composable 73 + private fun ScheduleButton( 74 + meal: MealSchedule, 75 + isCurrent: Boolean, 76 + onClick: () -> Unit, 77 + modifier: Modifier = Modifier 78 + ) { 79 + val startTime = formatTime(meal.startHour, meal.startMinute) 80 + val endTime = formatTime(meal.endHour, meal.endMinute) 81 + val accessibilityLabel = stringResource( 82 + R.string.meal_time_range, 83 + meal.phase.displayName, 84 + startTime, 85 + endTime 86 + ) + if (isCurrent) ", ${stringResource(R.string.current_meal)}" else "" 87 + 88 + val buttonContent: @Composable () -> Unit = { 89 + Column( 90 + horizontalAlignment = Alignment.CenterHorizontally, 91 + verticalArrangement = Arrangement.spacedBy(4.dp), 92 + modifier = Modifier 93 + .padding(vertical = 8.dp) 94 + .semantics { contentDescription = accessibilityLabel } 95 + ) { 96 + Icon( 97 + imageVector = meal.phase.icon, 98 + contentDescription = null 99 + ) 100 + Text( 101 + text = meal.phase.displayName, 102 + style = MaterialTheme.typography.labelMedium, 103 + fontWeight = FontWeight.Medium 104 + ) 105 + Text( 106 + text = "$startTime-$endTime", 107 + style = MaterialTheme.typography.labelSmall, 108 + color = MaterialTheme.colorScheme.onSurfaceVariant 109 + ) 110 + } 111 + } 112 + 113 + if (isCurrent) { 114 + FilledTonalButton( 115 + onClick = onClick, 116 + modifier = modifier, 117 + border = BorderStroke(2.dp, StatusOpen) 118 + ) { 119 + buttonContent() 120 + } 121 + } else { 122 + OutlinedButton( 123 + onClick = onClick, 124 + modifier = modifier 125 + ) { 126 + buttonContent() 127 + } 128 + } 129 + } 130 + 131 + private fun formatTime(hour: Int, minute: Int): String { 132 + val period = if (hour >= 12) "PM" else "AM" 133 + val displayHour = when { 134 + hour > 12 -> hour - 12 135 + hour == 0 -> 12 136 + else -> hour 137 + } 138 + return if (minute == 0) { 139 + "$displayHour$period" 140 + } else { 141 + "$displayHour:${minute.toString().padStart(2, '0')}$period" 142 + } 143 + }
+120
android/app/src/main/java/com/wasupchucks/ui/components/StatusCard.kt
··· 1 + package com.wasupchucks.ui.components 2 + 3 + import androidx.compose.animation.animateContentSize 4 + import androidx.compose.foundation.layout.Arrangement 5 + import androidx.compose.foundation.layout.Column 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.material3.ElevatedCard 12 + import androidx.compose.material3.Icon 13 + import androidx.compose.material3.MaterialTheme 14 + import androidx.compose.material3.Text 15 + import androidx.compose.runtime.Composable 16 + import androidx.compose.ui.Alignment 17 + import androidx.compose.ui.Modifier 18 + import androidx.compose.ui.res.stringResource 19 + import androidx.compose.ui.semantics.contentDescription 20 + import androidx.compose.ui.semantics.semantics 21 + import androidx.compose.ui.text.font.FontWeight 22 + import androidx.compose.ui.unit.dp 23 + import com.wasupchucks.R 24 + import com.wasupchucks.data.model.ChucksStatus 25 + import com.wasupchucks.data.model.MealPhase 26 + import com.wasupchucks.data.model.toExpandedCountdown 27 + import com.wasupchucks.ui.theme.CountdownTypography 28 + import com.wasupchucks.ui.theme.StatusClosed 29 + import com.wasupchucks.ui.theme.StatusOpen 30 + 31 + @Composable 32 + fun StatusCard( 33 + status: ChucksStatus, 34 + modifier: Modifier = Modifier 35 + ) { 36 + val statusColor = if (status.isOpen) StatusOpen else StatusClosed 37 + val statusIcon = if (status.isOpen) { 38 + status.currentPhase.icon 39 + } else { 40 + status.nextPhase?.icon ?: MealPhase.CLOSED.icon 41 + } 42 + val statusText = if (status.isOpen) { 43 + stringResource(R.string.status_open) 44 + } else { 45 + stringResource(R.string.status_closed) 46 + } 47 + val mealText = if (status.isOpen) { 48 + status.currentPhase.displayName 49 + } else { 50 + status.nextPhase?.displayName ?: "" 51 + } 52 + 53 + val accessibilityLabel = if (status.isOpen) { 54 + stringResource(R.string.open_for_meal, status.currentPhase.displayName) 55 + } else { 56 + stringResource(R.string.closed_next_meal, status.nextPhase?.displayName ?: "") 57 + } 58 + 59 + ElevatedCard( 60 + modifier = modifier.fillMaxWidth() 61 + ) { 62 + Column( 63 + modifier = Modifier 64 + .fillMaxWidth() 65 + .padding(16.dp) 66 + .animateContentSize() 67 + ) { 68 + Row( 69 + modifier = Modifier 70 + .fillMaxWidth() 71 + .semantics { contentDescription = accessibilityLabel }, 72 + verticalAlignment = Alignment.CenterVertically, 73 + horizontalArrangement = Arrangement.spacedBy(8.dp) 74 + ) { 75 + Icon( 76 + imageVector = statusIcon, 77 + contentDescription = null, 78 + tint = statusColor 79 + ) 80 + Column { 81 + Text( 82 + text = statusText, 83 + style = MaterialTheme.typography.titleSmall, 84 + fontWeight = FontWeight.SemiBold, 85 + color = statusColor 86 + ) 87 + Text( 88 + text = mealText, 89 + style = MaterialTheme.typography.bodySmall, 90 + color = MaterialTheme.colorScheme.onSurfaceVariant 91 + ) 92 + } 93 + } 94 + 95 + status.timeRemaining?.let { remaining -> 96 + Spacer(modifier = Modifier.height(8.dp)) 97 + 98 + Text( 99 + text = remaining.toExpandedCountdown(), 100 + style = CountdownTypography.large, 101 + color = MaterialTheme.colorScheme.onSurface, 102 + modifier = Modifier.align(Alignment.CenterHorizontally) 103 + ) 104 + 105 + val untilText = if (status.isOpen) { 106 + stringResource(R.string.until_ends, status.currentPhase.displayName) 107 + } else { 108 + stringResource(R.string.until_opens, status.nextPhase?.displayName ?: "open") 109 + } 110 + 111 + Text( 112 + text = untilText, 113 + style = MaterialTheme.typography.bodyMedium, 114 + color = MaterialTheme.colorScheme.onSurfaceVariant, 115 + modifier = Modifier.align(Alignment.CenterHorizontally) 116 + ) 117 + } 118 + } 119 + } 120 + }
+123
android/app/src/main/java/com/wasupchucks/ui/components/VenueCard.kt
··· 1 + package com.wasupchucks.ui.components 2 + 3 + import androidx.compose.animation.AnimatedVisibility 4 + import androidx.compose.animation.expandVertically 5 + import androidx.compose.animation.fadeIn 6 + import androidx.compose.animation.fadeOut 7 + import androidx.compose.animation.shrinkVertically 8 + import androidx.compose.foundation.background 9 + import androidx.compose.foundation.clickable 10 + import androidx.compose.foundation.layout.Arrangement 11 + import androidx.compose.foundation.layout.Box 12 + import androidx.compose.foundation.layout.Column 13 + import androidx.compose.foundation.layout.Row 14 + import androidx.compose.foundation.layout.Spacer 15 + import androidx.compose.foundation.layout.fillMaxWidth 16 + import androidx.compose.foundation.layout.height 17 + import androidx.compose.foundation.layout.padding 18 + import androidx.compose.foundation.layout.size 19 + import androidx.compose.foundation.shape.CircleShape 20 + import androidx.compose.foundation.shape.RoundedCornerShape 21 + import androidx.compose.material.icons.Icons 22 + import androidx.compose.material.icons.filled.ExpandLess 23 + import androidx.compose.material.icons.filled.ExpandMore 24 + import androidx.compose.material3.ElevatedCard 25 + import androidx.compose.material3.Icon 26 + import androidx.compose.material3.MaterialTheme 27 + import androidx.compose.material3.Text 28 + import androidx.compose.runtime.Composable 29 + import androidx.compose.runtime.getValue 30 + import androidx.compose.runtime.mutableStateOf 31 + import androidx.compose.runtime.saveable.rememberSaveable 32 + import androidx.compose.runtime.setValue 33 + import androidx.compose.ui.Alignment 34 + import androidx.compose.ui.Modifier 35 + import androidx.compose.ui.draw.clip 36 + import androidx.compose.ui.semantics.contentDescription 37 + import androidx.compose.ui.semantics.semantics 38 + import androidx.compose.ui.text.font.FontWeight 39 + import androidx.compose.ui.unit.dp 40 + import com.wasupchucks.data.model.VenueMenu 41 + import com.wasupchucks.ui.theme.StatusClosed 42 + 43 + @Composable 44 + fun VenueCard( 45 + venue: VenueMenu, 46 + modifier: Modifier = Modifier 47 + ) { 48 + var isExpanded by rememberSaveable { mutableStateOf(true) } 49 + 50 + ElevatedCard( 51 + modifier = modifier.fillMaxWidth() 52 + ) { 53 + Column( 54 + modifier = Modifier 55 + .fillMaxWidth() 56 + .clip(RoundedCornerShape(12.dp)) 57 + ) { 58 + // Header 59 + Row( 60 + modifier = Modifier 61 + .fillMaxWidth() 62 + .clickable { isExpanded = !isExpanded } 63 + .padding(16.dp), 64 + verticalAlignment = Alignment.CenterVertically, 65 + horizontalArrangement = Arrangement.SpaceBetween 66 + ) { 67 + Text( 68 + text = venue.venue, 69 + style = MaterialTheme.typography.titleSmall, 70 + fontWeight = FontWeight.SemiBold, 71 + color = StatusClosed 72 + ) 73 + Icon( 74 + imageVector = if (isExpanded) Icons.Filled.ExpandLess else Icons.Filled.ExpandMore, 75 + contentDescription = if (isExpanded) "Collapse" else "Expand", 76 + tint = StatusClosed 77 + ) 78 + } 79 + 80 + // Content 81 + AnimatedVisibility( 82 + visible = isExpanded, 83 + enter = fadeIn() + expandVertically(), 84 + exit = fadeOut() + shrinkVertically() 85 + ) { 86 + Column( 87 + modifier = Modifier 88 + .fillMaxWidth() 89 + .padding(start = 16.dp, end = 16.dp, bottom = 16.dp), 90 + verticalArrangement = Arrangement.spacedBy(8.dp) 91 + ) { 92 + venue.items.forEach { item -> 93 + Row( 94 + modifier = Modifier 95 + .fillMaxWidth() 96 + .semantics { contentDescription = item.name }, 97 + verticalAlignment = Alignment.CenterVertically, 98 + horizontalArrangement = Arrangement.spacedBy(8.dp) 99 + ) { 100 + // Bullet point 101 + Box( 102 + modifier = Modifier 103 + .size(4.dp) 104 + .background( 105 + MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), 106 + CircleShape 107 + ) 108 + ) 109 + 110 + Text( 111 + text = item.name, 112 + style = MaterialTheme.typography.bodyMedium, 113 + modifier = Modifier.weight(1f) 114 + ) 115 + 116 + AllergenRow(allergens = item.allergens) 117 + } 118 + } 119 + } 120 + } 121 + } 122 + } 123 + }
+310
android/app/src/main/java/com/wasupchucks/ui/screens/home/HomeScreen.kt
··· 1 + package com.wasupchucks.ui.screens.home 2 + 3 + import android.content.Intent 4 + import android.net.Uri 5 + import androidx.compose.foundation.layout.Arrangement 6 + import androidx.compose.foundation.layout.Box 7 + import androidx.compose.foundation.layout.Column 8 + import androidx.compose.foundation.layout.Row 9 + import androidx.compose.foundation.layout.Spacer 10 + import androidx.compose.foundation.layout.WindowInsets 11 + import androidx.compose.foundation.layout.fillMaxSize 12 + import androidx.compose.foundation.layout.fillMaxWidth 13 + import androidx.compose.foundation.layout.height 14 + import androidx.compose.foundation.layout.padding 15 + import androidx.compose.foundation.layout.widthIn 16 + import androidx.compose.foundation.lazy.LazyColumn 17 + import androidx.compose.foundation.lazy.items 18 + import androidx.compose.material.icons.Icons 19 + import androidx.compose.material.icons.filled.Schedule 20 + import androidx.compose.material3.CircularProgressIndicator 21 + import androidx.compose.material3.ExperimentalMaterial3Api 22 + import androidx.compose.material3.HorizontalDivider 23 + import androidx.compose.material3.Icon 24 + import androidx.compose.material3.LargeTopAppBar 25 + import androidx.compose.material3.MaterialTheme 26 + import androidx.compose.material3.Scaffold 27 + import androidx.compose.material3.Text 28 + import androidx.compose.material3.TextButton 29 + import androidx.compose.material3.TopAppBarDefaults 30 + import androidx.compose.material3.pulltorefresh.PullToRefreshBox 31 + import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass 32 + import androidx.compose.runtime.Composable 33 + import androidx.compose.runtime.getValue 34 + import androidx.compose.ui.Alignment 35 + import androidx.compose.ui.Modifier 36 + import androidx.compose.ui.input.nestedscroll.nestedScroll 37 + import androidx.compose.ui.platform.LocalContext 38 + import androidx.compose.ui.res.stringResource 39 + import androidx.compose.ui.text.font.FontWeight 40 + import androidx.compose.ui.text.style.TextAlign 41 + import androidx.compose.ui.unit.dp 42 + import androidx.hilt.navigation.compose.hiltViewModel 43 + import androidx.lifecycle.compose.collectAsStateWithLifecycle 44 + import com.wasupchucks.R 45 + import com.wasupchucks.ui.components.ErrorCard 46 + import com.wasupchucks.ui.components.MealDetailSheet 47 + import com.wasupchucks.ui.components.ScheduleCard 48 + import com.wasupchucks.ui.components.StatusCard 49 + import com.wasupchucks.ui.components.VenueCard 50 + 51 + @OptIn(ExperimentalMaterial3Api::class) 52 + @Composable 53 + fun HomeScreen( 54 + widthSizeClass: WindowWidthSizeClass, 55 + viewModel: HomeViewModel = hiltViewModel() 56 + ) { 57 + val uiState by viewModel.uiState.collectAsStateWithLifecycle() 58 + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() 59 + val context = LocalContext.current 60 + 61 + val isExpandedWidth = widthSizeClass == WindowWidthSizeClass.Expanded || 62 + widthSizeClass == WindowWidthSizeClass.Medium 63 + 64 + Scaffold( 65 + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), 66 + topBar = { 67 + LargeTopAppBar( 68 + title = { Text(stringResource(R.string.app_name)) }, 69 + scrollBehavior = scrollBehavior, 70 + colors = TopAppBarDefaults.largeTopAppBarColors( 71 + scrolledContainerColor = MaterialTheme.colorScheme.surface 72 + ) 73 + ) 74 + }, 75 + contentWindowInsets = WindowInsets(0, 0, 0, 0) 76 + ) { paddingValues -> 77 + PullToRefreshBox( 78 + isRefreshing = uiState.isRefreshing, 79 + onRefresh = { viewModel.refresh() }, 80 + modifier = Modifier 81 + .fillMaxSize() 82 + .padding(paddingValues) 83 + ) { 84 + LazyColumn( 85 + modifier = Modifier 86 + .fillMaxSize() 87 + .padding(horizontal = 16.dp), 88 + horizontalAlignment = Alignment.CenterHorizontally, 89 + verticalArrangement = Arrangement.spacedBy(16.dp) 90 + ) { 91 + item { 92 + Spacer(modifier = Modifier.height(8.dp)) 93 + } 94 + 95 + // Status and Schedule cards 96 + item { 97 + if (isExpandedWidth) { 98 + Row( 99 + modifier = Modifier 100 + .widthIn(max = 900.dp) 101 + .fillMaxWidth(), 102 + horizontalArrangement = Arrangement.spacedBy(16.dp) 103 + ) { 104 + StatusCard( 105 + status = uiState.status, 106 + modifier = Modifier.weight(1f) 107 + ) 108 + ScheduleCard( 109 + status = uiState.status, 110 + schedule = uiState.todaySchedule, 111 + onMealClick = { viewModel.selectMeal(it) }, 112 + modifier = Modifier.weight(1f) 113 + ) 114 + } 115 + } else { 116 + Column( 117 + modifier = Modifier.fillMaxWidth(), 118 + verticalArrangement = Arrangement.spacedBy(16.dp) 119 + ) { 120 + StatusCard(status = uiState.status) 121 + ScheduleCard( 122 + status = uiState.status, 123 + schedule = uiState.todaySchedule, 124 + onMealClick = { viewModel.selectMeal(it) } 125 + ) 126 + } 127 + } 128 + } 129 + 130 + // Loading state 131 + if (uiState.isLoading) { 132 + item { 133 + Box( 134 + modifier = Modifier 135 + .fillMaxWidth() 136 + .height(200.dp), 137 + contentAlignment = Alignment.Center 138 + ) { 139 + CircularProgressIndicator() 140 + } 141 + } 142 + } 143 + 144 + // Error state 145 + uiState.error?.let { error -> 146 + item { 147 + ErrorCard( 148 + error = error, 149 + onRetry = { viewModel.loadMenu() }, 150 + modifier = Modifier.widthIn(max = 900.dp) 151 + ) 152 + } 153 + } 154 + 155 + // Menu content 156 + if (!uiState.isLoading && uiState.error == null) { 157 + // Meal Specials Section 158 + if (uiState.mealSpecificVenues.isNotEmpty()) { 159 + item { 160 + Row( 161 + modifier = Modifier 162 + .widthIn(max = 900.dp) 163 + .fillMaxWidth() 164 + .padding(horizontal = 4.dp), 165 + verticalAlignment = Alignment.CenterVertically, 166 + horizontalArrangement = Arrangement.spacedBy(8.dp) 167 + ) { 168 + Icon( 169 + imageVector = Icons.Filled.Schedule, 170 + contentDescription = null, 171 + tint = MaterialTheme.colorScheme.primary 172 + ) 173 + Text( 174 + text = stringResource(R.string.meal_specials, getMealLabel(uiState.currentSlot)), 175 + style = MaterialTheme.typography.titleMedium, 176 + fontWeight = FontWeight.SemiBold 177 + ) 178 + } 179 + } 180 + 181 + if (isExpandedWidth) { 182 + // Two-column layout for expanded width 183 + val chunkedVenues = uiState.mealSpecificVenues.chunked(2) 184 + items(chunkedVenues, key = { it.map { v -> v.id }.joinToString() }) { rowVenues -> 185 + Row( 186 + modifier = Modifier 187 + .widthIn(max = 900.dp) 188 + .fillMaxWidth(), 189 + horizontalArrangement = Arrangement.spacedBy(16.dp) 190 + ) { 191 + rowVenues.forEach { venue -> 192 + VenueCard( 193 + venue = venue, 194 + modifier = Modifier.weight(1f) 195 + ) 196 + } 197 + if (rowVenues.size == 1) { 198 + Spacer(modifier = Modifier.weight(1f)) 199 + } 200 + } 201 + } 202 + } else { 203 + items(uiState.mealSpecificVenues, key = { it.id }) { venue -> 204 + VenueCard(venue = venue) 205 + } 206 + } 207 + } 208 + 209 + // Always Available Section 210 + if (uiState.alwaysAvailableVenues.isNotEmpty()) { 211 + item { 212 + Row( 213 + modifier = Modifier 214 + .widthIn(max = 900.dp) 215 + .fillMaxWidth() 216 + .padding(top = 8.dp), 217 + verticalAlignment = Alignment.CenterVertically, 218 + horizontalArrangement = Arrangement.spacedBy(12.dp) 219 + ) { 220 + HorizontalDivider(modifier = Modifier.weight(1f)) 221 + Text( 222 + text = stringResource(R.string.always_available), 223 + style = MaterialTheme.typography.labelMedium, 224 + color = MaterialTheme.colorScheme.onSurfaceVariant 225 + ) 226 + HorizontalDivider(modifier = Modifier.weight(1f)) 227 + } 228 + } 229 + 230 + if (isExpandedWidth) { 231 + val chunkedVenues = uiState.alwaysAvailableVenues.chunked(2) 232 + items(chunkedVenues, key = { it.map { v -> v.id }.joinToString() + "-always" }) { rowVenues -> 233 + Row( 234 + modifier = Modifier 235 + .widthIn(max = 900.dp) 236 + .fillMaxWidth(), 237 + horizontalArrangement = Arrangement.spacedBy(16.dp) 238 + ) { 239 + rowVenues.forEach { venue -> 240 + VenueCard( 241 + venue = venue, 242 + modifier = Modifier.weight(1f) 243 + ) 244 + } 245 + if (rowVenues.size == 1) { 246 + Spacer(modifier = Modifier.weight(1f)) 247 + } 248 + } 249 + } 250 + } else { 251 + items(uiState.alwaysAvailableVenues, key = { "${it.id}-always" }) { venue -> 252 + VenueCard(venue = venue) 253 + } 254 + } 255 + } 256 + } 257 + 258 + // Footer 259 + item { 260 + Column( 261 + modifier = Modifier 262 + .fillMaxWidth() 263 + .padding(vertical = 16.dp), 264 + horizontalAlignment = Alignment.CenterHorizontally 265 + ) { 266 + Text( 267 + text = stringResource(R.string.made_with_love), 268 + style = MaterialTheme.typography.labelSmall, 269 + color = MaterialTheme.colorScheme.onSurfaceVariant 270 + ) 271 + TextButton( 272 + onClick = { 273 + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(context.getString(R.string.privacy_policy_url))) 274 + context.startActivity(intent) 275 + } 276 + ) { 277 + Text( 278 + text = stringResource(R.string.privacy_policy), 279 + style = MaterialTheme.typography.labelSmall 280 + ) 281 + } 282 + } 283 + } 284 + 285 + item { 286 + Spacer(modifier = Modifier.height(16.dp)) 287 + } 288 + } 289 + } 290 + } 291 + 292 + // Meal Detail Sheet 293 + uiState.selectedMeal?.let { meal -> 294 + MealDetailSheet( 295 + meal = meal, 296 + menu = uiState.todayMenu, 297 + onDismiss = { viewModel.selectMeal(null) } 298 + ) 299 + } 300 + } 301 + 302 + @Composable 303 + private fun getMealLabel(slot: String): String { 304 + return when (slot) { 305 + "breakfast" -> stringResource(R.string.breakfast) 306 + "lunch" -> stringResource(R.string.lunch) 307 + "dinner" -> stringResource(R.string.dinner) 308 + else -> "This Meal" 309 + } 310 + }
+32
android/app/src/main/java/com/wasupchucks/ui/screens/home/HomeUiState.kt
··· 1 + package com.wasupchucks.ui.screens.home 2 + 3 + import com.wasupchucks.data.model.ChucksStatus 4 + import com.wasupchucks.data.model.MealSchedule 5 + import com.wasupchucks.data.model.VenueMenu 6 + 7 + data class HomeUiState( 8 + val status: ChucksStatus = ChucksStatus.calculate(), 9 + val todayMenu: List<VenueMenu> = emptyList(), 10 + val todaySchedule: List<MealSchedule> = MealSchedule.scheduleForToday(), 11 + val isLoading: Boolean = true, 12 + val error: Throwable? = null, 13 + val selectedMeal: MealSchedule? = null, 14 + val isRefreshing: Boolean = false 15 + ) { 16 + val currentSlot: String 17 + get() = if (status.isOpen) { 18 + status.currentPhase.apiSlot 19 + } else { 20 + status.nextPhase?.apiSlot ?: "lunch" 21 + } 22 + 23 + val mealSpecificVenues: List<VenueMenu> 24 + get() = todayMenu 25 + .filter { it.slot == currentSlot } 26 + .sortedBy { it.venue } 27 + 28 + val alwaysAvailableVenues: List<VenueMenu> 29 + get() = todayMenu 30 + .filter { it.slot == "anytime" } 31 + .sortedBy { it.venue } 32 + }
+104
android/app/src/main/java/com/wasupchucks/ui/screens/home/HomeViewModel.kt
··· 1 + package com.wasupchucks.ui.screens.home 2 + 3 + import androidx.lifecycle.ViewModel 4 + import androidx.lifecycle.viewModelScope 5 + import com.wasupchucks.data.model.ChucksStatus 6 + import com.wasupchucks.data.model.MealSchedule 7 + import com.wasupchucks.data.repository.MenuRepository 8 + import dagger.hilt.android.lifecycle.HiltViewModel 9 + import kotlinx.coroutines.delay 10 + import kotlinx.coroutines.flow.MutableStateFlow 11 + import kotlinx.coroutines.flow.StateFlow 12 + import kotlinx.coroutines.flow.asStateFlow 13 + import kotlinx.coroutines.flow.update 14 + import kotlinx.coroutines.launch 15 + import java.time.LocalDate 16 + import java.time.ZoneId 17 + import javax.inject.Inject 18 + 19 + @HiltViewModel 20 + class HomeViewModel @Inject constructor( 21 + private val menuRepository: MenuRepository 22 + ) : ViewModel() { 23 + 24 + private val _uiState = MutableStateFlow(HomeUiState()) 25 + val uiState: StateFlow<HomeUiState> = _uiState.asStateFlow() 26 + 27 + init { 28 + loadMenu() 29 + startStatusTimer() 30 + } 31 + 32 + private fun startStatusTimer() { 33 + viewModelScope.launch { 34 + while (true) { 35 + _uiState.update { it.copy(status = ChucksStatus.calculate()) } 36 + delay(1000L) 37 + } 38 + } 39 + } 40 + 41 + fun loadMenu() { 42 + viewModelScope.launch { 43 + _uiState.update { it.copy(isLoading = true, error = null) } 44 + 45 + val cedarvilleZone = ZoneId.of("America/New_York") 46 + val today = LocalDate.now(cedarvilleZone) 47 + 48 + menuRepository.getMenuForDate(today) 49 + .onSuccess { menu -> 50 + _uiState.update { 51 + it.copy( 52 + todayMenu = menu, 53 + todaySchedule = MealSchedule.scheduleForToday(), 54 + isLoading = false, 55 + error = null 56 + ) 57 + } 58 + } 59 + .onFailure { error -> 60 + _uiState.update { 61 + it.copy( 62 + isLoading = false, 63 + error = error 64 + ) 65 + } 66 + } 67 + } 68 + } 69 + 70 + fun refresh() { 71 + viewModelScope.launch { 72 + _uiState.update { it.copy(isRefreshing = true) } 73 + 74 + menuRepository.invalidateCache() 75 + 76 + val cedarvilleZone = ZoneId.of("America/New_York") 77 + val today = LocalDate.now(cedarvilleZone) 78 + 79 + menuRepository.getMenuForDate(today) 80 + .onSuccess { menu -> 81 + _uiState.update { 82 + it.copy( 83 + todayMenu = menu, 84 + todaySchedule = MealSchedule.scheduleForToday(), 85 + isRefreshing = false, 86 + error = null 87 + ) 88 + } 89 + } 90 + .onFailure { error -> 91 + _uiState.update { 92 + it.copy( 93 + isRefreshing = false, 94 + error = error 95 + ) 96 + } 97 + } 98 + } 99 + } 100 + 101 + fun selectMeal(meal: MealSchedule?) { 102 + _uiState.update { it.copy(selectedMeal = meal) } 103 + } 104 + }
+83
android/app/src/main/java/com/wasupchucks/ui/theme/Color.kt
··· 1 + package com.wasupchucks.ui.theme 2 + 3 + import androidx.compose.ui.graphics.Color 4 + 5 + // Primary brand colors 6 + val ChucksGreen = Color(0xFF4CAF50) 7 + val ChucksGreenDark = Color(0xFF388E3C) 8 + val ChucksOrange = Color(0xFFFF9800) 9 + val ChucksOrangeDark = Color(0xFFF57C00) 10 + 11 + // Status colors 12 + val StatusOpen = ChucksGreen 13 + val StatusClosed = ChucksOrange 14 + val StatusOpenContainer = Color(0xFFE8F5E9) 15 + val StatusClosedContainer = Color(0xFFFFF3E0) 16 + 17 + // Allergen colors 18 + val AllergenDietary = ChucksGreen 19 + val AllergenWarning = ChucksOrange 20 + val AllergenDietaryContainer = Color(0xFFE8F5E9) 21 + val AllergenWarningContainer = Color(0xFFFFF3E0) 22 + 23 + // Light theme colors 24 + val md_theme_light_primary = Color(0xFF4CAF50) 25 + val md_theme_light_onPrimary = Color(0xFFFFFFFF) 26 + val md_theme_light_primaryContainer = Color(0xFFB8F5B1) 27 + val md_theme_light_onPrimaryContainer = Color(0xFF002204) 28 + val md_theme_light_secondary = Color(0xFF52634F) 29 + val md_theme_light_onSecondary = Color(0xFFFFFFFF) 30 + val md_theme_light_secondaryContainer = Color(0xFFD5E8CF) 31 + val md_theme_light_onSecondaryContainer = Color(0xFF101F10) 32 + val md_theme_light_tertiary = Color(0xFF39656B) 33 + val md_theme_light_onTertiary = Color(0xFFFFFFFF) 34 + val md_theme_light_tertiaryContainer = Color(0xFFBCEBF1) 35 + val md_theme_light_onTertiaryContainer = Color(0xFF001F23) 36 + val md_theme_light_error = Color(0xFFBA1A1A) 37 + val md_theme_light_errorContainer = Color(0xFFFFDAD6) 38 + val md_theme_light_onError = Color(0xFFFFFFFF) 39 + val md_theme_light_onErrorContainer = Color(0xFF410002) 40 + val md_theme_light_background = Color(0xFFFCFDF6) 41 + val md_theme_light_onBackground = Color(0xFF1A1C19) 42 + val md_theme_light_surface = Color(0xFFFCFDF6) 43 + val md_theme_light_onSurface = Color(0xFF1A1C19) 44 + val md_theme_light_surfaceVariant = Color(0xFFDEE5D8) 45 + val md_theme_light_onSurfaceVariant = Color(0xFF424940) 46 + val md_theme_light_outline = Color(0xFF72796F) 47 + val md_theme_light_inverseOnSurface = Color(0xFFF0F1EB) 48 + val md_theme_light_inverseSurface = Color(0xFF2F312D) 49 + val md_theme_light_inversePrimary = Color(0xFF9DD897) 50 + val md_theme_light_surfaceTint = Color(0xFF4CAF50) 51 + val md_theme_light_outlineVariant = Color(0xFFC2C9BD) 52 + val md_theme_light_scrim = Color(0xFF000000) 53 + 54 + // Dark theme colors 55 + val md_theme_dark_primary = Color(0xFF9DD897) 56 + val md_theme_dark_onPrimary = Color(0xFF00390B) 57 + val md_theme_dark_primaryContainer = Color(0xFF005315) 58 + val md_theme_dark_onPrimaryContainer = Color(0xFFB8F5B1) 59 + val md_theme_dark_secondary = Color(0xFFB9CCB3) 60 + val md_theme_dark_onSecondary = Color(0xFF253423) 61 + val md_theme_dark_secondaryContainer = Color(0xFF3B4B39) 62 + val md_theme_dark_onSecondaryContainer = Color(0xFFD5E8CF) 63 + val md_theme_dark_tertiary = Color(0xFFA1CED5) 64 + val md_theme_dark_onTertiary = Color(0xFF00363C) 65 + val md_theme_dark_tertiaryContainer = Color(0xFF1F4D53) 66 + val md_theme_dark_onTertiaryContainer = Color(0xFFBCEBF1) 67 + val md_theme_dark_error = Color(0xFFFFB4AB) 68 + val md_theme_dark_errorContainer = Color(0xFF93000A) 69 + val md_theme_dark_onError = Color(0xFF690005) 70 + val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6) 71 + val md_theme_dark_background = Color(0xFF1A1C19) 72 + val md_theme_dark_onBackground = Color(0xFFE2E3DD) 73 + val md_theme_dark_surface = Color(0xFF1A1C19) 74 + val md_theme_dark_onSurface = Color(0xFFE2E3DD) 75 + val md_theme_dark_surfaceVariant = Color(0xFF424940) 76 + val md_theme_dark_onSurfaceVariant = Color(0xFFC2C9BD) 77 + val md_theme_dark_outline = Color(0xFF8C9388) 78 + val md_theme_dark_inverseOnSurface = Color(0xFF1A1C19) 79 + val md_theme_dark_inverseSurface = Color(0xFFE2E3DD) 80 + val md_theme_dark_inversePrimary = Color(0xFF006D21) 81 + val md_theme_dark_surfaceTint = Color(0xFF9DD897) 82 + val md_theme_dark_outlineVariant = Color(0xFF424940) 83 + val md_theme_dark_scrim = Color(0xFF000000)
+97
android/app/src/main/java/com/wasupchucks/ui/theme/Theme.kt
··· 1 + package com.wasupchucks.ui.theme 2 + 3 + import android.os.Build 4 + import androidx.compose.foundation.isSystemInDarkTheme 5 + import androidx.compose.material3.MaterialTheme 6 + import androidx.compose.material3.darkColorScheme 7 + import androidx.compose.material3.dynamicDarkColorScheme 8 + import androidx.compose.material3.dynamicLightColorScheme 9 + import androidx.compose.material3.lightColorScheme 10 + import androidx.compose.runtime.Composable 11 + import androidx.compose.ui.platform.LocalContext 12 + 13 + private val LightColorScheme = lightColorScheme( 14 + primary = md_theme_light_primary, 15 + onPrimary = md_theme_light_onPrimary, 16 + primaryContainer = md_theme_light_primaryContainer, 17 + onPrimaryContainer = md_theme_light_onPrimaryContainer, 18 + secondary = md_theme_light_secondary, 19 + onSecondary = md_theme_light_onSecondary, 20 + secondaryContainer = md_theme_light_secondaryContainer, 21 + onSecondaryContainer = md_theme_light_onSecondaryContainer, 22 + tertiary = md_theme_light_tertiary, 23 + onTertiary = md_theme_light_onTertiary, 24 + tertiaryContainer = md_theme_light_tertiaryContainer, 25 + onTertiaryContainer = md_theme_light_onTertiaryContainer, 26 + error = md_theme_light_error, 27 + errorContainer = md_theme_light_errorContainer, 28 + onError = md_theme_light_onError, 29 + onErrorContainer = md_theme_light_onErrorContainer, 30 + background = md_theme_light_background, 31 + onBackground = md_theme_light_onBackground, 32 + surface = md_theme_light_surface, 33 + onSurface = md_theme_light_onSurface, 34 + surfaceVariant = md_theme_light_surfaceVariant, 35 + onSurfaceVariant = md_theme_light_onSurfaceVariant, 36 + outline = md_theme_light_outline, 37 + inverseOnSurface = md_theme_light_inverseOnSurface, 38 + inverseSurface = md_theme_light_inverseSurface, 39 + inversePrimary = md_theme_light_inversePrimary, 40 + surfaceTint = md_theme_light_surfaceTint, 41 + outlineVariant = md_theme_light_outlineVariant, 42 + scrim = md_theme_light_scrim 43 + ) 44 + 45 + private val DarkColorScheme = darkColorScheme( 46 + primary = md_theme_dark_primary, 47 + onPrimary = md_theme_dark_onPrimary, 48 + primaryContainer = md_theme_dark_primaryContainer, 49 + onPrimaryContainer = md_theme_dark_onPrimaryContainer, 50 + secondary = md_theme_dark_secondary, 51 + onSecondary = md_theme_dark_onSecondary, 52 + secondaryContainer = md_theme_dark_secondaryContainer, 53 + onSecondaryContainer = md_theme_dark_onSecondaryContainer, 54 + tertiary = md_theme_dark_tertiary, 55 + onTertiary = md_theme_dark_onTertiary, 56 + tertiaryContainer = md_theme_dark_tertiaryContainer, 57 + onTertiaryContainer = md_theme_dark_onTertiaryContainer, 58 + error = md_theme_dark_error, 59 + errorContainer = md_theme_dark_errorContainer, 60 + onError = md_theme_dark_onError, 61 + onErrorContainer = md_theme_dark_onErrorContainer, 62 + background = md_theme_dark_background, 63 + onBackground = md_theme_dark_onBackground, 64 + surface = md_theme_dark_surface, 65 + onSurface = md_theme_dark_onSurface, 66 + surfaceVariant = md_theme_dark_surfaceVariant, 67 + onSurfaceVariant = md_theme_dark_onSurfaceVariant, 68 + outline = md_theme_dark_outline, 69 + inverseOnSurface = md_theme_dark_inverseOnSurface, 70 + inverseSurface = md_theme_dark_inverseSurface, 71 + inversePrimary = md_theme_dark_inversePrimary, 72 + surfaceTint = md_theme_dark_surfaceTint, 73 + outlineVariant = md_theme_dark_outlineVariant, 74 + scrim = md_theme_dark_scrim 75 + ) 76 + 77 + @Composable 78 + fun WasupChucksTheme( 79 + darkTheme: Boolean = isSystemInDarkTheme(), 80 + dynamicColor: Boolean = true, 81 + content: @Composable () -> Unit 82 + ) { 83 + val colorScheme = when { 84 + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 85 + val context = LocalContext.current 86 + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 87 + } 88 + darkTheme -> DarkColorScheme 89 + else -> LightColorScheme 90 + } 91 + 92 + MaterialTheme( 93 + colorScheme = colorScheme, 94 + typography = Typography, 95 + content = content 96 + ) 97 + }
+142
android/app/src/main/java/com/wasupchucks/ui/theme/Type.kt
··· 1 + package com.wasupchucks.ui.theme 2 + 3 + import androidx.compose.material3.Typography 4 + import androidx.compose.ui.text.TextStyle 5 + import androidx.compose.ui.text.font.FontFamily 6 + import androidx.compose.ui.text.font.FontWeight 7 + import androidx.compose.ui.unit.sp 8 + 9 + val Typography = Typography( 10 + displayLarge = TextStyle( 11 + fontFamily = FontFamily.Default, 12 + fontWeight = FontWeight.Bold, 13 + fontSize = 57.sp, 14 + lineHeight = 64.sp, 15 + letterSpacing = (-0.25).sp 16 + ), 17 + displayMedium = TextStyle( 18 + fontFamily = FontFamily.Default, 19 + fontWeight = FontWeight.Bold, 20 + fontSize = 45.sp, 21 + lineHeight = 52.sp, 22 + letterSpacing = 0.sp 23 + ), 24 + displaySmall = TextStyle( 25 + fontFamily = FontFamily.Default, 26 + fontWeight = FontWeight.Bold, 27 + fontSize = 36.sp, 28 + lineHeight = 44.sp, 29 + letterSpacing = 0.sp 30 + ), 31 + headlineLarge = TextStyle( 32 + fontFamily = FontFamily.Default, 33 + fontWeight = FontWeight.Bold, 34 + fontSize = 32.sp, 35 + lineHeight = 40.sp, 36 + letterSpacing = 0.sp 37 + ), 38 + headlineMedium = TextStyle( 39 + fontFamily = FontFamily.Default, 40 + fontWeight = FontWeight.SemiBold, 41 + fontSize = 28.sp, 42 + lineHeight = 36.sp, 43 + letterSpacing = 0.sp 44 + ), 45 + headlineSmall = TextStyle( 46 + fontFamily = FontFamily.Default, 47 + fontWeight = FontWeight.SemiBold, 48 + fontSize = 24.sp, 49 + lineHeight = 32.sp, 50 + letterSpacing = 0.sp 51 + ), 52 + titleLarge = TextStyle( 53 + fontFamily = FontFamily.Default, 54 + fontWeight = FontWeight.SemiBold, 55 + fontSize = 22.sp, 56 + lineHeight = 28.sp, 57 + letterSpacing = 0.sp 58 + ), 59 + titleMedium = TextStyle( 60 + fontFamily = FontFamily.Default, 61 + fontWeight = FontWeight.Medium, 62 + fontSize = 16.sp, 63 + lineHeight = 24.sp, 64 + letterSpacing = 0.15.sp 65 + ), 66 + titleSmall = TextStyle( 67 + fontFamily = FontFamily.Default, 68 + fontWeight = FontWeight.Medium, 69 + fontSize = 14.sp, 70 + lineHeight = 20.sp, 71 + letterSpacing = 0.1.sp 72 + ), 73 + bodyLarge = TextStyle( 74 + fontFamily = FontFamily.Default, 75 + fontWeight = FontWeight.Normal, 76 + fontSize = 16.sp, 77 + lineHeight = 24.sp, 78 + letterSpacing = 0.5.sp 79 + ), 80 + bodyMedium = TextStyle( 81 + fontFamily = FontFamily.Default, 82 + fontWeight = FontWeight.Normal, 83 + fontSize = 14.sp, 84 + lineHeight = 20.sp, 85 + letterSpacing = 0.25.sp 86 + ), 87 + bodySmall = TextStyle( 88 + fontFamily = FontFamily.Default, 89 + fontWeight = FontWeight.Normal, 90 + fontSize = 12.sp, 91 + lineHeight = 16.sp, 92 + letterSpacing = 0.4.sp 93 + ), 94 + labelLarge = TextStyle( 95 + fontFamily = FontFamily.Default, 96 + fontWeight = FontWeight.Medium, 97 + fontSize = 14.sp, 98 + lineHeight = 20.sp, 99 + letterSpacing = 0.1.sp 100 + ), 101 + labelMedium = TextStyle( 102 + fontFamily = FontFamily.Default, 103 + fontWeight = FontWeight.Medium, 104 + fontSize = 12.sp, 105 + lineHeight = 16.sp, 106 + letterSpacing = 0.5.sp 107 + ), 108 + labelSmall = TextStyle( 109 + fontFamily = FontFamily.Default, 110 + fontWeight = FontWeight.Medium, 111 + fontSize = 11.sp, 112 + lineHeight = 16.sp, 113 + letterSpacing = 0.5.sp 114 + ) 115 + ) 116 + 117 + // Custom text styles for countdown display 118 + object CountdownTypography { 119 + val large = TextStyle( 120 + fontFamily = FontFamily.Default, 121 + fontWeight = FontWeight.Bold, 122 + fontSize = 64.sp, 123 + lineHeight = 72.sp, 124 + letterSpacing = (-1).sp 125 + ) 126 + 127 + val medium = TextStyle( 128 + fontFamily = FontFamily.Default, 129 + fontWeight = FontWeight.Bold, 130 + fontSize = 48.sp, 131 + lineHeight = 56.sp, 132 + letterSpacing = (-0.5).sp 133 + ) 134 + 135 + val small = TextStyle( 136 + fontFamily = FontFamily.Default, 137 + fontWeight = FontWeight.Bold, 138 + fontSize = 44.sp, 139 + lineHeight = 52.sp, 140 + letterSpacing = (-0.5).sp 141 + ) 142 + }
+10
android/app/src/main/java/com/wasupchucks/util/CedarvilleTime.kt
··· 1 + package com.wasupchucks.util 2 + 3 + import java.time.ZoneId 4 + import java.time.ZonedDateTime 5 + 6 + object CedarvilleTime { 7 + val zoneId: ZoneId = ZoneId.of("America/New_York") 8 + 9 + fun now(): ZonedDateTime = ZonedDateTime.now(zoneId) 10 + }
+93
android/app/src/main/java/com/wasupchucks/widget/ChucksWidget.kt
··· 1 + package com.wasupchucks.widget 2 + 3 + import android.content.Context 4 + import androidx.glance.GlanceId 5 + import androidx.glance.GlanceModifier 6 + import androidx.glance.GlanceTheme 7 + import androidx.glance.appwidget.GlanceAppWidget 8 + import androidx.glance.appwidget.GlanceAppWidgetReceiver 9 + import androidx.glance.appwidget.provideContent 10 + import androidx.glance.background 11 + import androidx.glance.layout.fillMaxSize 12 + import com.wasupchucks.data.model.ChucksStatus 13 + import com.wasupchucks.widget.ui.LargeWidgetContent 14 + import com.wasupchucks.widget.ui.MediumWidgetContent 15 + import com.wasupchucks.widget.ui.SmallWidgetContent 16 + 17 + // Small Widget 18 + class ChucksSmallWidget : GlanceAppWidget() { 19 + override suspend fun provideGlance(context: Context, id: GlanceId) { 20 + val status = ChucksStatus.calculate() 21 + 22 + provideContent { 23 + GlanceTheme { 24 + SmallWidgetContent( 25 + status = status 26 + ) 27 + } 28 + } 29 + } 30 + } 31 + 32 + class ChucksWidgetReceiver : GlanceAppWidgetReceiver() { 33 + override val glanceAppWidget: GlanceAppWidget = ChucksSmallWidget() 34 + 35 + override fun onEnabled(context: Context) { 36 + super.onEnabled(context) 37 + WidgetRefreshWorker.enqueue(context) 38 + } 39 + } 40 + 41 + // Medium Widget 42 + class ChucksMediumWidget : GlanceAppWidget() { 43 + override suspend fun provideGlance(context: Context, id: GlanceId) { 44 + val status = ChucksStatus.calculate() 45 + val widgetData = WidgetState.load(context) 46 + 47 + provideContent { 48 + GlanceTheme { 49 + MediumWidgetContent( 50 + status = status, 51 + specials = widgetData.specials, 52 + venueName = widgetData.venueName 53 + ) 54 + } 55 + } 56 + } 57 + } 58 + 59 + class ChucksMediumWidgetReceiver : GlanceAppWidgetReceiver() { 60 + override val glanceAppWidget: GlanceAppWidget = ChucksMediumWidget() 61 + 62 + override fun onEnabled(context: Context) { 63 + super.onEnabled(context) 64 + WidgetRefreshWorker.enqueue(context) 65 + } 66 + } 67 + 68 + // Large Widget 69 + class ChucksLargeWidget : GlanceAppWidget() { 70 + override suspend fun provideGlance(context: Context, id: GlanceId) { 71 + val status = ChucksStatus.calculate() 72 + val widgetData = WidgetState.load(context) 73 + 74 + provideContent { 75 + GlanceTheme { 76 + LargeWidgetContent( 77 + status = status, 78 + specials = widgetData.specials, 79 + venueName = widgetData.venueName 80 + ) 81 + } 82 + } 83 + } 84 + } 85 + 86 + class ChucksLargeWidgetReceiver : GlanceAppWidgetReceiver() { 87 + override val glanceAppWidget: GlanceAppWidget = ChucksLargeWidget() 88 + 89 + override fun onEnabled(context: Context) { 90 + super.onEnabled(context) 91 + WidgetRefreshWorker.enqueue(context) 92 + } 93 + }
+74
android/app/src/main/java/com/wasupchucks/widget/WidgetRefreshWorker.kt
··· 1 + package com.wasupchucks.widget 2 + 3 + import android.content.Context 4 + import androidx.glance.appwidget.updateAll 5 + import androidx.hilt.work.HiltWorker 6 + import androidx.work.CoroutineWorker 7 + import androidx.work.ExistingPeriodicWorkPolicy 8 + import androidx.work.PeriodicWorkRequestBuilder 9 + import androidx.work.WorkManager 10 + import androidx.work.WorkerParameters 11 + import com.wasupchucks.data.model.ChucksStatus 12 + import com.wasupchucks.data.model.MealPhase 13 + import com.wasupchucks.data.repository.MenuRepository 14 + import dagger.assisted.Assisted 15 + import dagger.assisted.AssistedInject 16 + import java.time.LocalDate 17 + import java.time.ZoneId 18 + import java.util.concurrent.TimeUnit 19 + 20 + @HiltWorker 21 + class WidgetRefreshWorker @AssistedInject constructor( 22 + @Assisted private val context: Context, 23 + @Assisted params: WorkerParameters, 24 + private val menuRepository: MenuRepository 25 + ) : CoroutineWorker(context, params) { 26 + 27 + override suspend fun doWork(): Result { 28 + return try { 29 + val status = ChucksStatus.calculate() 30 + val phase = if (status.isOpen) status.currentPhase else (status.nextPhase ?: MealPhase.LUNCH) 31 + val cedarvilleZone = ZoneId.of("America/New_York") 32 + val today = LocalDate.now(cedarvilleZone) 33 + 34 + if (phase != MealPhase.CLOSED) { 35 + menuRepository.getSpecialsWithVenue(today, phase) 36 + .onSuccess { (items, venueName) -> 37 + WidgetState.save( 38 + context, 39 + WidgetData( 40 + specials = items, 41 + venueName = venueName, 42 + lastUpdate = System.currentTimeMillis() 43 + ) 44 + ) 45 + } 46 + } 47 + 48 + // Update all widget instances 49 + ChucksSmallWidget().updateAll(context) 50 + ChucksMediumWidget().updateAll(context) 51 + ChucksLargeWidget().updateAll(context) 52 + 53 + Result.success() 54 + } catch (e: Exception) { 55 + Result.retry() 56 + } 57 + } 58 + 59 + companion object { 60 + private const val WORK_NAME = "widget_refresh" 61 + 62 + fun enqueue(context: Context) { 63 + val request = PeriodicWorkRequestBuilder<WidgetRefreshWorker>( 64 + 15, TimeUnit.MINUTES 65 + ).build() 66 + 67 + WorkManager.getInstance(context).enqueueUniquePeriodicWork( 68 + WORK_NAME, 69 + ExistingPeriodicWorkPolicy.KEEP, 70 + request 71 + ) 72 + } 73 + } 74 + }
+60
android/app/src/main/java/com/wasupchucks/widget/WidgetState.kt
··· 1 + package com.wasupchucks.widget 2 + 3 + import android.content.Context 4 + import androidx.datastore.preferences.core.Preferences 5 + import androidx.datastore.preferences.core.edit 6 + import androidx.datastore.preferences.core.longPreferencesKey 7 + import androidx.datastore.preferences.core.stringPreferencesKey 8 + import androidx.datastore.preferences.preferencesDataStore 9 + import com.squareup.moshi.Moshi 10 + import com.squareup.moshi.Types 11 + import com.wasupchucks.data.model.MenuItem 12 + import kotlinx.coroutines.flow.first 13 + import kotlinx.coroutines.flow.map 14 + 15 + private val Context.widgetDataStore by preferencesDataStore(name = "widget_data") 16 + 17 + data class WidgetData( 18 + val specials: List<MenuItem> = emptyList(), 19 + val venueName: String = "Home Cooking", 20 + val lastUpdate: Long = 0 21 + ) 22 + 23 + object WidgetState { 24 + private val SPECIALS_KEY = stringPreferencesKey("specials_json") 25 + private val VENUE_NAME_KEY = stringPreferencesKey("venue_name") 26 + private val LAST_UPDATE_KEY = longPreferencesKey("last_update") 27 + 28 + private val moshi = Moshi.Builder().build() 29 + private val menuItemListType = Types.newParameterizedType(List::class.java, MenuItem::class.java) 30 + private val menuItemListAdapter = moshi.adapter<List<MenuItem>>(menuItemListType) 31 + 32 + suspend fun save(context: Context, data: WidgetData) { 33 + context.widgetDataStore.edit { prefs -> 34 + prefs[SPECIALS_KEY] = menuItemListAdapter.toJson(data.specials) 35 + prefs[VENUE_NAME_KEY] = data.venueName 36 + prefs[LAST_UPDATE_KEY] = data.lastUpdate 37 + } 38 + } 39 + 40 + suspend fun load(context: Context): WidgetData { 41 + return context.widgetDataStore.data.map { prefs -> 42 + val specialsJson = prefs[SPECIALS_KEY] 43 + val specials = if (specialsJson != null) { 44 + try { 45 + menuItemListAdapter.fromJson(specialsJson) ?: emptyList() 46 + } catch (e: Exception) { 47 + emptyList() 48 + } 49 + } else { 50 + emptyList() 51 + } 52 + 53 + WidgetData( 54 + specials = specials, 55 + venueName = prefs[VENUE_NAME_KEY] ?: "Home Cooking", 56 + lastUpdate = prefs[LAST_UPDATE_KEY] ?: 0 57 + ) 58 + }.first() 59 + } 60 + }
+190
android/app/src/main/java/com/wasupchucks/widget/ui/LargeWidget.kt
··· 1 + package com.wasupchucks.widget.ui 2 + 3 + import androidx.compose.runtime.Composable 4 + import androidx.compose.ui.unit.dp 5 + import androidx.compose.ui.unit.sp 6 + import androidx.glance.GlanceModifier 7 + import androidx.glance.GlanceTheme 8 + import androidx.glance.Image 9 + import androidx.glance.ImageProvider 10 + import androidx.glance.layout.Alignment 11 + import androidx.glance.layout.Column 12 + import androidx.glance.layout.Row 13 + import androidx.glance.layout.Spacer 14 + import androidx.glance.layout.fillMaxSize 15 + import androidx.glance.layout.fillMaxWidth 16 + import androidx.glance.layout.height 17 + import androidx.glance.layout.padding 18 + import androidx.glance.layout.size 19 + import androidx.glance.layout.width 20 + import androidx.glance.text.FontWeight 21 + import androidx.glance.text.Text 22 + import androidx.glance.text.TextAlign 23 + import androidx.glance.text.TextStyle 24 + import com.wasupchucks.R 25 + import com.wasupchucks.data.model.ChucksStatus 26 + import com.wasupchucks.data.model.MealPhase 27 + import com.wasupchucks.data.model.MenuItem 28 + import com.wasupchucks.data.model.toCompactCountdown 29 + 30 + @Composable 31 + fun LargeWidgetContent( 32 + status: ChucksStatus, 33 + specials: List<MenuItem>, 34 + venueName: String 35 + ) { 36 + val statusColor = if (status.isOpen) { 37 + GlanceTheme.colors.primary 38 + } else { 39 + GlanceTheme.colors.error 40 + } 41 + 42 + val iconRes = when { 43 + status.isOpen -> when (status.currentPhase) { 44 + MealPhase.BREAKFAST -> R.drawable.ic_breakfast 45 + MealPhase.LUNCH -> R.drawable.ic_lunch 46 + MealPhase.DINNER -> R.drawable.ic_dinner 47 + MealPhase.CLOSED -> R.drawable.ic_closed 48 + } 49 + status.nextPhase != null -> when (status.nextPhase) { 50 + MealPhase.BREAKFAST -> R.drawable.ic_breakfast 51 + MealPhase.LUNCH -> R.drawable.ic_lunch 52 + MealPhase.DINNER -> R.drawable.ic_dinner 53 + MealPhase.CLOSED -> R.drawable.ic_closed 54 + } 55 + else -> R.drawable.ic_closed 56 + } 57 + 58 + Column( 59 + modifier = GlanceModifier 60 + .fillMaxSize() 61 + .padding(16.dp) 62 + ) { 63 + // Header row 64 + Row( 65 + modifier = GlanceModifier.fillMaxWidth(), 66 + verticalAlignment = Alignment.CenterVertically 67 + ) { 68 + // Status info 69 + Row( 70 + modifier = GlanceModifier.defaultWeight(), 71 + verticalAlignment = Alignment.CenterVertically 72 + ) { 73 + Image( 74 + provider = ImageProvider(iconRes), 75 + contentDescription = null, 76 + modifier = GlanceModifier.size(28.dp) 77 + ) 78 + Spacer(modifier = GlanceModifier.width(8.dp)) 79 + Column { 80 + Text( 81 + text = if (status.isOpen) "Open" else "Closed", 82 + style = TextStyle( 83 + fontSize = 12.sp, 84 + fontWeight = FontWeight.Bold, 85 + color = statusColor 86 + ) 87 + ) 88 + val mealName = if (status.isOpen) { 89 + status.currentPhase.displayName 90 + } else { 91 + status.nextPhase?.displayName ?: "" 92 + } 93 + Text( 94 + text = mealName, 95 + style = TextStyle( 96 + fontSize = 18.sp, 97 + fontWeight = FontWeight.Bold, 98 + color = GlanceTheme.colors.onSurface 99 + ) 100 + ) 101 + } 102 + } 103 + 104 + // Countdown 105 + status.timeRemaining?.let { remaining -> 106 + Column( 107 + horizontalAlignment = Alignment.End 108 + ) { 109 + Text( 110 + text = remaining.toCompactCountdown(), 111 + style = TextStyle( 112 + fontSize = 44.sp, 113 + fontWeight = FontWeight.Bold, 114 + color = GlanceTheme.colors.onSurface, 115 + textAlign = TextAlign.End 116 + ) 117 + ) 118 + val labelText = when { 119 + status.isOpen -> "until ${status.currentPhase.displayName} ends" 120 + else -> "until open" 121 + } 122 + Text( 123 + text = labelText, 124 + style = TextStyle( 125 + fontSize = 11.sp, 126 + color = GlanceTheme.colors.onSurfaceVariant, 127 + textAlign = TextAlign.End 128 + ) 129 + ) 130 + } 131 + } 132 + } 133 + 134 + Spacer(modifier = GlanceModifier.height(12.dp)) 135 + 136 + // Divider simulation 137 + Spacer( 138 + modifier = GlanceModifier 139 + .fillMaxWidth() 140 + .height(1.dp) 141 + ) 142 + 143 + Spacer(modifier = GlanceModifier.height(12.dp)) 144 + 145 + // Specials section 146 + Text( 147 + text = venueName, 148 + style = TextStyle( 149 + fontSize = 12.sp, 150 + fontWeight = FontWeight.Bold, 151 + color = GlanceTheme.colors.onSurfaceVariant 152 + ) 153 + ) 154 + 155 + Spacer(modifier = GlanceModifier.height(8.dp)) 156 + 157 + if (specials.isEmpty()) { 158 + Spacer(modifier = GlanceModifier.defaultWeight()) 159 + Row( 160 + modifier = GlanceModifier.fillMaxWidth(), 161 + horizontalAlignment = Alignment.CenterHorizontally 162 + ) { 163 + Text( 164 + text = "No specials available", 165 + style = TextStyle( 166 + fontSize = 14.sp, 167 + color = GlanceTheme.colors.onSurfaceVariant 168 + ) 169 + ) 170 + } 171 + Spacer(modifier = GlanceModifier.defaultWeight()) 172 + } else { 173 + Column( 174 + modifier = GlanceModifier.fillMaxWidth() 175 + ) { 176 + specials.take(6).forEach { item -> 177 + Text( 178 + text = "\u2022 ${item.name}", 179 + style = TextStyle( 180 + fontSize = 14.sp, 181 + color = GlanceTheme.colors.onSurface 182 + ), 183 + maxLines = 1 184 + ) 185 + Spacer(modifier = GlanceModifier.height(4.dp)) 186 + } 187 + } 188 + } 189 + } 190 + }
+171
android/app/src/main/java/com/wasupchucks/widget/ui/MediumWidget.kt
··· 1 + package com.wasupchucks.widget.ui 2 + 3 + import androidx.compose.runtime.Composable 4 + import androidx.compose.ui.unit.dp 5 + import androidx.compose.ui.unit.sp 6 + import androidx.glance.GlanceModifier 7 + import androidx.glance.GlanceTheme 8 + import androidx.glance.Image 9 + import androidx.glance.ImageProvider 10 + import androidx.glance.layout.Alignment 11 + import androidx.glance.layout.Column 12 + import androidx.glance.layout.Row 13 + import androidx.glance.layout.Spacer 14 + import androidx.glance.layout.fillMaxHeight 15 + import androidx.glance.layout.fillMaxSize 16 + import androidx.glance.layout.fillMaxWidth 17 + import androidx.glance.layout.height 18 + import androidx.glance.layout.padding 19 + import androidx.glance.layout.size 20 + import androidx.glance.layout.width 21 + import androidx.glance.text.FontWeight 22 + import androidx.glance.text.Text 23 + import androidx.glance.text.TextStyle 24 + import com.wasupchucks.R 25 + import com.wasupchucks.data.model.ChucksStatus 26 + import com.wasupchucks.data.model.MealPhase 27 + import com.wasupchucks.data.model.MenuItem 28 + import com.wasupchucks.data.model.toCompactCountdown 29 + 30 + @Composable 31 + fun MediumWidgetContent( 32 + status: ChucksStatus, 33 + specials: List<MenuItem>, 34 + venueName: String 35 + ) { 36 + val statusColor = if (status.isOpen) { 37 + GlanceTheme.colors.primary 38 + } else { 39 + GlanceTheme.colors.error 40 + } 41 + 42 + val iconRes = when { 43 + status.isOpen -> when (status.currentPhase) { 44 + MealPhase.BREAKFAST -> R.drawable.ic_breakfast 45 + MealPhase.LUNCH -> R.drawable.ic_lunch 46 + MealPhase.DINNER -> R.drawable.ic_dinner 47 + MealPhase.CLOSED -> R.drawable.ic_closed 48 + } 49 + status.nextPhase != null -> when (status.nextPhase) { 50 + MealPhase.BREAKFAST -> R.drawable.ic_breakfast 51 + MealPhase.LUNCH -> R.drawable.ic_lunch 52 + MealPhase.DINNER -> R.drawable.ic_dinner 53 + MealPhase.CLOSED -> R.drawable.ic_closed 54 + } 55 + else -> R.drawable.ic_closed 56 + } 57 + 58 + Row( 59 + modifier = GlanceModifier 60 + .fillMaxSize() 61 + .padding(12.dp), 62 + verticalAlignment = Alignment.CenterVertically 63 + ) { 64 + // Left side - Status 65 + Column( 66 + modifier = GlanceModifier 67 + .defaultWeight() 68 + .fillMaxHeight(), 69 + horizontalAlignment = Alignment.CenterHorizontally, 70 + verticalAlignment = Alignment.CenterVertically 71 + ) { 72 + Row( 73 + verticalAlignment = Alignment.CenterVertically 74 + ) { 75 + Image( 76 + provider = ImageProvider(iconRes), 77 + contentDescription = null, 78 + modifier = GlanceModifier.size(16.dp) 79 + ) 80 + Spacer(modifier = GlanceModifier.width(4.dp)) 81 + Text( 82 + text = if (status.isOpen) "Open" else "Closed", 83 + style = TextStyle( 84 + fontSize = 12.sp, 85 + fontWeight = FontWeight.Bold, 86 + color = statusColor 87 + ) 88 + ) 89 + } 90 + 91 + status.timeRemaining?.let { remaining -> 92 + Text( 93 + text = remaining.toCompactCountdown(), 94 + style = TextStyle( 95 + fontSize = 40.sp, 96 + fontWeight = FontWeight.Bold, 97 + color = GlanceTheme.colors.onSurface 98 + ) 99 + ) 100 + } 101 + 102 + val labelText = when { 103 + status.isOpen -> "until ${status.currentPhase.displayName} ends" 104 + status.nextPhase != null && status.nextPhase != MealPhase.CLOSED -> "until ${status.nextPhase.displayName}" 105 + else -> "" 106 + } 107 + 108 + if (labelText.isNotEmpty()) { 109 + Text( 110 + text = labelText, 111 + style = TextStyle( 112 + fontSize = 10.sp, 113 + color = GlanceTheme.colors.onSurfaceVariant 114 + ) 115 + ) 116 + } 117 + } 118 + 119 + // Divider 120 + Spacer( 121 + modifier = GlanceModifier 122 + .width(1.dp) 123 + .fillMaxHeight() 124 + .padding(vertical = 8.dp) 125 + ) 126 + 127 + Spacer(modifier = GlanceModifier.width(12.dp)) 128 + 129 + // Right side - Specials 130 + Column( 131 + modifier = GlanceModifier 132 + .defaultWeight() 133 + .fillMaxHeight(), 134 + verticalAlignment = Alignment.Top 135 + ) { 136 + Text( 137 + text = venueName, 138 + style = TextStyle( 139 + fontSize = 11.sp, 140 + fontWeight = FontWeight.Bold, 141 + color = GlanceTheme.colors.onSurfaceVariant 142 + ) 143 + ) 144 + 145 + Spacer(modifier = GlanceModifier.height(4.dp)) 146 + 147 + if (specials.isEmpty()) { 148 + Spacer(modifier = GlanceModifier.defaultWeight()) 149 + Text( 150 + text = "No specials available", 151 + style = TextStyle( 152 + fontSize = 11.sp, 153 + color = GlanceTheme.colors.onSurfaceVariant 154 + ) 155 + ) 156 + Spacer(modifier = GlanceModifier.defaultWeight()) 157 + } else { 158 + specials.take(4).forEach { item -> 159 + Text( 160 + text = "\u2022 ${item.name}", 161 + style = TextStyle( 162 + fontSize = 11.sp, 163 + color = GlanceTheme.colors.onSurface 164 + ), 165 + maxLines = 1 166 + ) 167 + } 168 + } 169 + } 170 + } 171 + }
+116
android/app/src/main/java/com/wasupchucks/widget/ui/SmallWidget.kt
··· 1 + package com.wasupchucks.widget.ui 2 + 3 + import androidx.compose.runtime.Composable 4 + import androidx.compose.ui.unit.dp 5 + import androidx.compose.ui.unit.sp 6 + import androidx.glance.GlanceModifier 7 + import androidx.glance.GlanceTheme 8 + import androidx.glance.Image 9 + import androidx.glance.ImageProvider 10 + import androidx.glance.layout.Alignment 11 + import androidx.glance.layout.Box 12 + import androidx.glance.layout.Column 13 + import androidx.glance.layout.Row 14 + import androidx.glance.layout.Spacer 15 + import androidx.glance.layout.fillMaxSize 16 + import androidx.glance.layout.fillMaxWidth 17 + import androidx.glance.layout.height 18 + import androidx.glance.layout.padding 19 + import androidx.glance.layout.size 20 + import androidx.glance.text.FontWeight 21 + import androidx.glance.text.Text 22 + import androidx.glance.text.TextStyle 23 + import com.wasupchucks.R 24 + import com.wasupchucks.data.model.ChucksStatus 25 + import com.wasupchucks.data.model.MealPhase 26 + import com.wasupchucks.data.model.toCompactCountdown 27 + 28 + @Composable 29 + fun SmallWidgetContent(status: ChucksStatus) { 30 + val statusColor = if (status.isOpen) { 31 + GlanceTheme.colors.primary 32 + } else { 33 + GlanceTheme.colors.error 34 + } 35 + 36 + val iconRes = when { 37 + status.isOpen -> when (status.currentPhase) { 38 + MealPhase.BREAKFAST -> R.drawable.ic_breakfast 39 + MealPhase.LUNCH -> R.drawable.ic_lunch 40 + MealPhase.DINNER -> R.drawable.ic_dinner 41 + MealPhase.CLOSED -> R.drawable.ic_closed 42 + } 43 + status.nextPhase != null -> when (status.nextPhase) { 44 + MealPhase.BREAKFAST -> R.drawable.ic_breakfast 45 + MealPhase.LUNCH -> R.drawable.ic_lunch 46 + MealPhase.DINNER -> R.drawable.ic_dinner 47 + MealPhase.CLOSED -> R.drawable.ic_closed 48 + } 49 + else -> R.drawable.ic_closed 50 + } 51 + 52 + Box( 53 + modifier = GlanceModifier 54 + .fillMaxSize() 55 + .padding(12.dp), 56 + contentAlignment = Alignment.Center 57 + ) { 58 + Column( 59 + modifier = GlanceModifier.fillMaxSize(), 60 + horizontalAlignment = Alignment.CenterHorizontally 61 + ) { 62 + // Status indicator 63 + Row( 64 + modifier = GlanceModifier.fillMaxWidth(), 65 + verticalAlignment = Alignment.CenterVertically, 66 + horizontalAlignment = Alignment.Start 67 + ) { 68 + Image( 69 + provider = ImageProvider(iconRes), 70 + contentDescription = null, 71 + modifier = GlanceModifier.size(20.dp) 72 + ) 73 + Spacer(modifier = GlanceModifier.size(4.dp)) 74 + Text( 75 + text = if (status.isOpen) "Open" else "Closed", 76 + style = TextStyle( 77 + fontSize = 12.sp, 78 + fontWeight = FontWeight.Bold, 79 + color = statusColor 80 + ) 81 + ) 82 + } 83 + 84 + Spacer(modifier = GlanceModifier.defaultWeight()) 85 + 86 + // Countdown 87 + status.timeRemaining?.let { remaining -> 88 + Text( 89 + text = remaining.toCompactCountdown(), 90 + style = TextStyle( 91 + fontSize = 48.sp, 92 + fontWeight = FontWeight.Bold, 93 + color = GlanceTheme.colors.onSurface 94 + ) 95 + ) 96 + } 97 + 98 + // Label 99 + val labelText = when { 100 + status.isOpen -> "until ${status.currentPhase.displayName} ends" 101 + status.nextPhase != null && status.nextPhase != MealPhase.CLOSED -> "until ${status.nextPhase.displayName}" 102 + else -> "See you tomorrow!" 103 + } 104 + 105 + Text( 106 + text = labelText, 107 + style = TextStyle( 108 + fontSize = 11.sp, 109 + color = GlanceTheme.colors.onSurfaceVariant 110 + ) 111 + ) 112 + 113 + Spacer(modifier = GlanceModifier.defaultWeight()) 114 + } 115 + } 116 + }
+11
android/app/src/main/res/drawable/ic_breakfast.xml
··· 1 + <?xml version="1.0" encoding="utf-8"?> 2 + <vector xmlns:android="http://schemas.android.com/apk/res/android" 3 + android:width="24dp" 4 + android:height="24dp" 5 + android:viewportWidth="24" 6 + android:viewportHeight="24" 7 + android:tint="?attr/colorControlNormal"> 8 + <path 9 + android:fillColor="@android:color/white" 10 + android:pathData="M20,3H4v10c0,2.21 1.79,4 4,4h6c2.21,0 4,-1.79 4,-4v-3h2c1.11,0 2,-0.9 2,-2V5C22,3.9 21.11,3 20,3zM20,8h-2V5h2V8zM4,19h16v2H4V19z"/> 11 + </vector>
+11
android/app/src/main/res/drawable/ic_closed.xml
··· 1 + <?xml version="1.0" encoding="utf-8"?> 2 + <vector xmlns:android="http://schemas.android.com/apk/res/android" 3 + android:width="24dp" 4 + android:height="24dp" 5 + android:viewportWidth="24" 6 + android:viewportHeight="24" 7 + android:tint="?attr/colorControlNormal"> 8 + <path 9 + android:fillColor="@android:color/white" 10 + android:pathData="M9.5,4c-4.97,0 -9,4.03 -9,9s4.03,9 9,9c0.78,0 1.53,-0.09 2.25,-0.26C10.06,20.31 9,18.28 9,16c0,-3.87 3.13,-7 7,-7c0.99,0 1.92,0.21 2.77,0.58C18.43,5.59 14.39,4 9.5,4zM12,14.25c-0.41,0 -0.75,-0.34 -0.75,-0.75s0.34,-0.75 0.75,-0.75s0.75,0.34 0.75,0.75S12.41,14.25 12,14.25zM14.75,11.75c-0.41,0 -0.75,-0.34 -0.75,-0.75s0.34,-0.75 0.75,-0.75S15.5,10.59 15.5,11S15.16,11.75 14.75,11.75z"/> 11 + </vector>
+11
android/app/src/main/res/drawable/ic_dinner.xml
··· 1 + <?xml version="1.0" encoding="utf-8"?> 2 + <vector xmlns:android="http://schemas.android.com/apk/res/android" 3 + android:width="24dp" 4 + android:height="24dp" 5 + android:viewportWidth="24" 6 + android:viewportHeight="24" 7 + android:tint="?attr/colorControlNormal"> 8 + <path 9 + android:fillColor="@android:color/white" 10 + android:pathData="M12,2C8.43,2 5.23,3.54 3.01,6L12,22l8.99,-16C18.78,3.55 15.57,2 12,2zM7,7c0,-0.55 0.45,-1 1,-1s1,0.45 1,1s-0.45,1 -1,1S7,7.55 7,7zM10,12c-0.55,0 -1,-0.45 -1,-1c0,-0.55 0.45,-1 1,-1s1,0.45 1,1C11,11.55 10.55,12 10,12zM12,9c-0.55,0 -1,-0.45 -1,-1c0,-0.55 0.45,-1 1,-1s1,0.45 1,1C13,8.55 12.55,9 12,9z"/> 11 + </vector>
+10
android/app/src/main/res/drawable/ic_launcher_background.xml
··· 1 + <?xml version="1.0" encoding="utf-8"?> 2 + <vector xmlns:android="http://schemas.android.com/apk/res/android" 3 + android:width="108dp" 4 + android:height="108dp" 5 + android:viewportWidth="108" 6 + android:viewportHeight="108"> 7 + <path 8 + android:fillColor="#4CAF50" 9 + android:pathData="M0,0h108v108h-108z"/> 10 + </vector>
+16
android/app/src/main/res/drawable/ic_launcher_foreground.xml
··· 1 + <?xml version="1.0" encoding="utf-8"?> 2 + <vector xmlns:android="http://schemas.android.com/apk/res/android" 3 + android:width="108dp" 4 + android:height="108dp" 5 + android:viewportWidth="108" 6 + android:viewportHeight="108"> 7 + <group android:scaleX="0.4" 8 + android:scaleY="0.4" 9 + android:translateX="32.4" 10 + android:translateY="32.4"> 11 + <!-- Fork and knife icon representing dining --> 12 + <path 13 + android:fillColor="#FFFFFF" 14 + android:pathData="M11,9H9V2H7v7H5V2H3v7c0,2.12 1.66,3.84 3.75,3.97V22h2.5v-9.03C11.34,12.84 13,11.12 13,9V2h-2V9zM16,6v8h2.5v8H21V2C18.24,2 16,4.24 16,6z"/> 15 + </group> 16 + </vector>
+11
android/app/src/main/res/drawable/ic_lunch.xml
··· 1 + <?xml version="1.0" encoding="utf-8"?> 2 + <vector xmlns:android="http://schemas.android.com/apk/res/android" 3 + android:width="24dp" 4 + android:height="24dp" 5 + android:viewportWidth="24" 6 + android:viewportHeight="24" 7 + android:tint="?attr/colorControlNormal"> 8 + <path 9 + android:fillColor="@android:color/white" 10 + android:pathData="M2,19h20v2H2V19zM5,7h14l-1.5,9h-11L5,7zM1,5h22v2H1V5z"/> 11 + </vector>
+6
android/app/src/main/res/drawable/widget_background.xml
··· 1 + <?xml version="1.0" encoding="utf-8"?> 2 + <shape xmlns:android="http://schemas.android.com/apk/res/android" 3 + android:shape="rectangle"> 4 + <solid android:color="#FFFFFF" /> 5 + <corners android:radius="24dp" /> 6 + </shape>
+12
android/app/src/main/res/layout/widget_loading.xml
··· 1 + <?xml version="1.0" encoding="utf-8"?> 2 + <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 + android:layout_width="match_parent" 4 + android:layout_height="match_parent" 5 + android:background="@android:color/transparent"> 6 + 7 + <ProgressBar 8 + android:layout_width="wrap_content" 9 + android:layout_height="wrap_content" 10 + android:layout_gravity="center" /> 11 + 12 + </FrameLayout>
+86
android/app/src/main/res/layout/widget_preview_large.xml
··· 1 + <?xml version="1.0" encoding="utf-8"?> 2 + <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 + android:layout_width="match_parent" 4 + android:layout_height="match_parent" 5 + android:background="@drawable/widget_background" 6 + android:orientation="vertical" 7 + android:padding="16dp"> 8 + 9 + <LinearLayout 10 + android:layout_width="match_parent" 11 + android:layout_height="wrap_content" 12 + android:gravity="center_vertical" 13 + android:orientation="horizontal"> 14 + 15 + <LinearLayout 16 + android:layout_width="0dp" 17 + android:layout_height="wrap_content" 18 + android:layout_weight="1" 19 + android:orientation="vertical"> 20 + 21 + <TextView 22 + android:layout_width="wrap_content" 23 + android:layout_height="wrap_content" 24 + android:text="Open" 25 + android:textColor="#4CAF50" 26 + android:textSize="14sp" 27 + android:textStyle="bold" /> 28 + 29 + <TextView 30 + android:layout_width="wrap_content" 31 + android:layout_height="wrap_content" 32 + android:text="Lunch" 33 + android:textColor="#333333" 34 + android:textSize="18sp" 35 + android:textStyle="bold" /> 36 + 37 + </LinearLayout> 38 + 39 + <LinearLayout 40 + android:layout_width="wrap_content" 41 + android:layout_height="wrap_content" 42 + android:gravity="end" 43 + android:orientation="vertical"> 44 + 45 + <TextView 46 + android:layout_width="wrap_content" 47 + android:layout_height="wrap_content" 48 + android:text="2h" 49 + android:textColor="#333333" 50 + android:textSize="40sp" 51 + android:textStyle="bold" /> 52 + 53 + <TextView 54 + android:layout_width="wrap_content" 55 + android:layout_height="wrap_content" 56 + android:text="until Lunch ends" 57 + android:textColor="#666666" 58 + android:textSize="12sp" /> 59 + 60 + </LinearLayout> 61 + 62 + </LinearLayout> 63 + 64 + <View 65 + android:layout_width="match_parent" 66 + android:layout_height="1dp" 67 + android:layout_marginVertical="12dp" 68 + android:background="#DDDDDD" /> 69 + 70 + <TextView 71 + android:layout_width="wrap_content" 72 + android:layout_height="wrap_content" 73 + android:text="Home Cooking" 74 + android:textColor="#666666" 75 + android:textSize="12sp" 76 + android:textStyle="bold" /> 77 + 78 + <TextView 79 + android:layout_width="wrap_content" 80 + android:layout_height="wrap_content" 81 + android:layout_marginTop="8dp" 82 + android:text="Scrambled Eggs\nSausage Patties\nTater Tots\nBiscuits\nCountry Gravy" 83 + android:textColor="#333333" 84 + android:textSize="14sp" /> 85 + 86 + </LinearLayout>
+65
android/app/src/main/res/layout/widget_preview_medium.xml
··· 1 + <?xml version="1.0" encoding="utf-8"?> 2 + <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 + android:layout_width="match_parent" 4 + android:layout_height="match_parent" 5 + android:background="@drawable/widget_background" 6 + android:orientation="horizontal" 7 + android:padding="16dp"> 8 + 9 + <LinearLayout 10 + android:layout_width="0dp" 11 + android:layout_height="match_parent" 12 + android:layout_weight="1" 13 + android:gravity="center" 14 + android:orientation="vertical"> 15 + 16 + <TextView 17 + android:layout_width="wrap_content" 18 + android:layout_height="wrap_content" 19 + android:text="Open" 20 + android:textColor="#4CAF50" 21 + android:textSize="14sp" 22 + android:textStyle="bold" /> 23 + 24 + <TextView 25 + android:layout_width="wrap_content" 26 + android:layout_height="wrap_content" 27 + android:text="2h" 28 + android:textColor="#4CAF50" 29 + android:textSize="40sp" 30 + android:textStyle="bold" /> 31 + 32 + </LinearLayout> 33 + 34 + <View 35 + android:layout_width="1dp" 36 + android:layout_height="match_parent" 37 + android:layout_marginHorizontal="8dp" 38 + android:background="#DDDDDD" /> 39 + 40 + <LinearLayout 41 + android:layout_width="0dp" 42 + android:layout_height="match_parent" 43 + android:layout_weight="1" 44 + android:orientation="vertical" 45 + android:padding="4dp"> 46 + 47 + <TextView 48 + android:layout_width="wrap_content" 49 + android:layout_height="wrap_content" 50 + android:text="Home Cooking" 51 + android:textColor="#666666" 52 + android:textSize="12sp" 53 + android:textStyle="bold" /> 54 + 55 + <TextView 56 + android:layout_width="wrap_content" 57 + android:layout_height="wrap_content" 58 + android:layout_marginTop="4dp" 59 + android:text="Scrambled Eggs\nSausage Patties\nTater Tots" 60 + android:textColor="#333333" 61 + android:textSize="11sp" /> 62 + 63 + </LinearLayout> 64 + 65 + </LinearLayout>
+25
android/app/src/main/res/layout/widget_preview_small.xml
··· 1 + <?xml version="1.0" encoding="utf-8"?> 2 + <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 + android:layout_width="match_parent" 4 + android:layout_height="match_parent" 5 + android:background="@drawable/widget_background" 6 + android:gravity="center" 7 + android:orientation="vertical" 8 + android:padding="16dp"> 9 + 10 + <TextView 11 + android:layout_width="wrap_content" 12 + android:layout_height="wrap_content" 13 + android:text="2h" 14 + android:textColor="#4CAF50" 15 + android:textSize="48sp" 16 + android:textStyle="bold" /> 17 + 18 + <TextView 19 + android:layout_width="wrap_content" 20 + android:layout_height="wrap_content" 21 + android:text="until Lunch ends" 22 + android:textColor="#666666" 23 + android:textSize="12sp" /> 24 + 25 + </LinearLayout>
+5
android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
··· 1 + <?xml version="1.0" encoding="utf-8"?> 2 + <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> 3 + <background android:drawable="@drawable/ic_launcher_background"/> 4 + <foreground android:drawable="@drawable/ic_launcher_foreground"/> 5 + </adaptive-icon>
+5
android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
··· 1 + <?xml version="1.0" encoding="utf-8"?> 2 + <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> 3 + <background android:drawable="@drawable/ic_launcher_background"/> 4 + <foreground android:drawable="@drawable/ic_launcher_foreground"/> 5 + </adaptive-icon>
+49
android/app/src/main/res/values/strings.xml
··· 1 + <?xml version="1.0" encoding="utf-8"?> 2 + <resources> 3 + <string name="app_name">Wasup Chuck\'s</string> 4 + <string name="widget_name_small">Chuck\'s Status</string> 5 + <string name="widget_name_medium">Chuck\'s Status &amp; Specials</string> 6 + <string name="widget_name_large">Chuck\'s Full Status</string> 7 + <string name="widget_description">See current meal times and specials at Chuck\'s.</string> 8 + 9 + <!-- Status --> 10 + <string name="status_open">Open</string> 11 + <string name="status_closed">Closed</string> 12 + <string name="until_ends">until %1$s ends</string> 13 + <string name="until_opens">until %1$s</string> 14 + <string name="see_you_tomorrow">See you tomorrow!</string> 15 + 16 + <!-- Meals --> 17 + <string name="breakfast">Breakfast</string> 18 + <string name="lunch">Lunch</string> 19 + <string name="dinner">Dinner</string> 20 + 21 + <!-- Sections --> 22 + <string name="todays_schedule">Today\'s Schedule</string> 23 + <string name="meal_specials">%1$s Specials</string> 24 + <string name="always_available">Always Available</string> 25 + <string name="no_specials">No specials available</string> 26 + <string name="no_menu_available">No Menu Available</string> 27 + <string name="no_menu_description">No specific menu items for %1$s today.</string> 28 + 29 + <!-- Errors --> 30 + <string name="error_title">Menu Unavailable</string> 31 + <string name="error_network">Couldn\'t connect to the server. Check your internet connection.</string> 32 + <string name="error_invalid_url">There was a problem with the request.</string> 33 + <string name="error_decoding">The menu data couldn\'t be read.</string> 34 + <string name="error_generic">Something went wrong loading the menu.</string> 35 + <string name="try_again">Try Again</string> 36 + 37 + <!-- Footer --> 38 + <string name="made_with_love">Made with \u2665 by Kieran Klukas</string> 39 + <string name="privacy_policy">Privacy Policy</string> 40 + <string name="privacy_policy_url">https://dunkirk.sh/wasup-chucks/</string> 41 + 42 + <!-- Accessibility --> 43 + <string name="open_for_meal">Chuck\'s is currently open for %1$s</string> 44 + <string name="closed_next_meal">Chuck\'s is closed. Next meal: %1$s</string> 45 + <string name="meal_time_range">%1$s, %2$s to %3$s</string> 46 + <string name="current_meal">current meal</string> 47 + <string name="tap_to_view_menu">Tap to view menu</string> 48 + <string name="contains_allergens">Contains: %1$s</string> 49 + </resources>
+8
android/app/src/main/res/values/themes.xml
··· 1 + <?xml version="1.0" encoding="utf-8"?> 2 + <resources> 3 + <style name="Theme.WasupChucks" parent="android:Theme.Material.Light.NoActionBar"> 4 + <item name="android:statusBarColor">@android:color/transparent</item> 5 + <item name="android:navigationBarColor">@android:color/transparent</item> 6 + <item name="android:windowLightStatusBar">true</item> 7 + </style> 8 + </resources>
+14
android/app/src/main/res/xml/chucks_widget_info_large.xml
··· 1 + <?xml version="1.0" encoding="utf-8"?> 2 + <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" 3 + android:minWidth="250dp" 4 + android:minHeight="250dp" 5 + android:targetCellWidth="4" 6 + android:targetCellHeight="4" 7 + android:maxResizeWidth="400dp" 8 + android:maxResizeHeight="400dp" 9 + android:updatePeriodMillis="900000" 10 + android:description="@string/widget_description" 11 + android:previewLayout="@layout/widget_preview_large" 12 + android:initialLayout="@layout/widget_loading" 13 + android:resizeMode="horizontal|vertical" 14 + android:widgetCategory="home_screen" />
+14
android/app/src/main/res/xml/chucks_widget_info_medium.xml
··· 1 + <?xml version="1.0" encoding="utf-8"?> 2 + <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" 3 + android:minWidth="250dp" 4 + android:minHeight="110dp" 5 + android:targetCellWidth="4" 6 + android:targetCellHeight="2" 7 + android:maxResizeWidth="400dp" 8 + android:maxResizeHeight="180dp" 9 + android:updatePeriodMillis="900000" 10 + android:description="@string/widget_description" 11 + android:previewLayout="@layout/widget_preview_medium" 12 + android:initialLayout="@layout/widget_loading" 13 + android:resizeMode="horizontal|vertical" 14 + android:widgetCategory="home_screen" />
+14
android/app/src/main/res/xml/chucks_widget_info_small.xml
··· 1 + <?xml version="1.0" encoding="utf-8"?> 2 + <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" 3 + android:minWidth="110dp" 4 + android:minHeight="110dp" 5 + android:targetCellWidth="2" 6 + android:targetCellHeight="2" 7 + android:maxResizeWidth="250dp" 8 + android:maxResizeHeight="250dp" 9 + android:updatePeriodMillis="900000" 10 + android:description="@string/widget_description" 11 + android:previewLayout="@layout/widget_preview_small" 12 + android:initialLayout="@layout/widget_loading" 13 + android:resizeMode="horizontal|vertical" 14 + android:widgetCategory="home_screen" />
+7
android/build.gradle.kts
··· 1 + plugins { 2 + alias(libs.plugins.android.application) apply false 3 + alias(libs.plugins.kotlin.android) apply false 4 + alias(libs.plugins.kotlin.compose) apply false 5 + alias(libs.plugins.hilt.android) apply false 6 + alias(libs.plugins.ksp) apply false 7 + }
+17
android/gradle.properties
··· 1 + # Project-wide Gradle settings. 2 + org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 3 + org.gradle.parallel=true 4 + org.gradle.caching=true 5 + org.gradle.configuration-cache=true 6 + 7 + # AndroidX package structure 8 + android.useAndroidX=true 9 + 10 + # Kotlin code style 11 + kotlin.code.style=official 12 + 13 + # Enables namespacing of each library's R class 14 + android.nonTransitiveRClass=true 15 + 16 + # Suppress warnings for Kotlin options 17 + kotlin.options.suppressFreeCompilerArgsModificationWarning=true
+76
android/gradle/libs.versions.toml
··· 1 + [versions] 2 + agp = "8.7.3" 3 + kotlin = "2.1.0" 4 + ksp = "2.1.0-1.0.29" 5 + coreKtx = "1.15.0" 6 + lifecycleRuntimeKtx = "2.8.7" 7 + activityCompose = "1.9.3" 8 + composeBom = "2025.01.00" 9 + material3 = "1.3.1" 10 + navigationCompose = "2.8.5" 11 + hilt = "2.54" 12 + hiltNavigationCompose = "1.2.0" 13 + retrofit = "2.11.0" 14 + moshi = "1.15.2" 15 + okhttp = "4.12.0" 16 + glance = "1.1.1" 17 + workManager = "2.10.0" 18 + datastore = "1.1.2" 19 + coroutines = "1.9.0" 20 + splashscreen = "1.0.1" 21 + material3WindowSizeClass = "1.3.1" 22 + 23 + [libraries] 24 + androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } 25 + androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } 26 + androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycleRuntimeKtx" } 27 + androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycleRuntimeKtx" } 28 + androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } 29 + androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } 30 + androidx-ui = { group = "androidx.compose.ui", name = "ui" } 31 + androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } 32 + androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } 33 + androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } 34 + androidx-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" } 35 + androidx-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" } 36 + androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" } 37 + androidx-material3-window-size-class = { group = "androidx.compose.material3", name = "material3-window-size-class", version.ref = "material3WindowSizeClass" } 38 + 39 + # Hilt 40 + hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } 41 + hilt-android-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" } 42 + hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hiltNavigationCompose" } 43 + hilt-work = { group = "androidx.hilt", name = "hilt-work", version.ref = "hiltNavigationCompose" } 44 + hilt-compiler = { group = "androidx.hilt", name = "hilt-compiler", version.ref = "hiltNavigationCompose" } 45 + 46 + # Networking 47 + retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" } 48 + retrofit-converter-moshi = { group = "com.squareup.retrofit2", name = "converter-moshi", version.ref = "retrofit" } 49 + moshi = { group = "com.squareup.moshi", name = "moshi", version.ref = "moshi" } 50 + moshi-kotlin-codegen = { group = "com.squareup.moshi", name = "moshi-kotlin-codegen", version.ref = "moshi" } 51 + okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } 52 + okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" } 53 + 54 + # Glance widgets 55 + androidx-glance = { group = "androidx.glance", name = "glance", version.ref = "glance" } 56 + androidx-glance-appwidget = { group = "androidx.glance", name = "glance-appwidget", version.ref = "glance" } 57 + androidx-glance-material3 = { group = "androidx.glance", name = "glance-material3", version.ref = "glance" } 58 + 59 + # WorkManager 60 + androidx-work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "workManager" } 61 + 62 + # DataStore 63 + androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" } 64 + 65 + # Coroutines 66 + kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" } 67 + 68 + # Splash screen 69 + androidx-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "splashscreen" } 70 + 71 + [plugins] 72 + android-application = { id = "com.android.application", version.ref = "agp" } 73 + kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } 74 + kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 75 + hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } 76 + ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
android/gradle/wrapper/gradle-wrapper.jar

This is a binary file and will not be displayed.

+7
android/gradle/wrapper/gradle-wrapper.properties
··· 1 + distributionBase=GRADLE_USER_HOME 2 + distributionPath=wrapper/dists 3 + distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip 4 + networkTimeout=10000 5 + validateDistributionUrl=true 6 + zipStoreBase=GRADLE_USER_HOME 7 + zipStorePath=wrapper/dists
+170
android/gradlew
··· 1 + #!/bin/sh 2 + 3 + # 4 + # Copyright 2015-2024 the original author or authors. 5 + # 6 + # Licensed under the Apache License, Version 2.0 (the "License"); 7 + # you may not use this file except in compliance with the License. 8 + # You may obtain a copy of the License at 9 + # 10 + # https://www.apache.org/licenses/LICENSE-2.0 11 + # 12 + 13 + ############################################################################## 14 + # 15 + # Gradle start up script for POSIX generated by Gradle. 16 + # 17 + ############################################################################## 18 + 19 + # Attempt to set APP_HOME 20 + 21 + # Resolve links: $0 may be a link 22 + app_path=$0 23 + 24 + # Need this for daisy-chained symlinks. 25 + while 26 + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 27 + [ -h "$app_path" ] 28 + do 29 + ls=$( ls -ld "$app_path" ) 30 + link=${ls#*' -> '} 31 + case $link in #( 32 + /*) app_path=$link ;; #( 33 + *) app_path=$APP_HOME$link ;; 34 + esac 35 + done 36 + 37 + APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit 38 + 39 + # Use the maximum available, or set MAX_FD != -1 to use that value. 40 + MAX_FD=maximum 41 + 42 + warn () { 43 + echo "$*" 44 + } >&2 45 + 46 + die () { 47 + echo 48 + echo "$*" 49 + echo 50 + exit 1 51 + } >&2 52 + 53 + # OS specific support (must be 'true' or 'false'). 54 + cygwin=false 55 + msys=false 56 + darwin=false 57 + nonstop=false 58 + case "$( uname )" in #( 59 + CYGWIN* ) cygwin=true ;; #( 60 + Darwin* ) darwin=true ;; #( 61 + MSYS* | MINGW* ) msys=true ;; #( 62 + NONSTOP* ) nonstop=true ;; 63 + esac 64 + 65 + CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 66 + 67 + 68 + # Determine the Java command to use to start the JVM. 69 + if [ -n "$JAVA_HOME" ] ; then 70 + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 71 + # IBM's JDK on AIX uses strange locations for the executables 72 + JAVACMD=$JAVA_HOME/jre/sh/java 73 + else 74 + JAVACMD=$JAVA_HOME/bin/java 75 + fi 76 + if [ ! -x "$JAVACMD" ] ; then 77 + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 78 + 79 + Please set the JAVA_HOME variable in your environment to match the 80 + location of your Java installation." 81 + fi 82 + else 83 + JAVACMD=java 84 + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 85 + 86 + Please set the JAVA_HOME variable in your environment to match the 87 + location of your Java installation." 88 + fi 89 + 90 + # Increase the maximum file descriptors if we can. 91 + if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 92 + case $MAX_FD in #( 93 + max*) 94 + MAX_FD=$( ulimit -H -n ) || 95 + warn "Could not query maximum file descriptor limit" 96 + esac 97 + case $MAX_FD in #( 98 + '' | soft) :;; #( 99 + *) 100 + ulimit -n "$MAX_FD" || 101 + warn "Could not set maximum file descriptor limit to $MAX_FD" 102 + esac 103 + fi 104 + 105 + # Collect all arguments for the java command, stacking in reverse order: 106 + # * args from the command line 107 + # * the main class name 108 + # * -classpath 109 + # * -D...appname settings 110 + # * --module-path (only if needed) 111 + # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 112 + 113 + # For Cygwin or MSYS, switch paths to Windows format before running java 114 + if "$cygwin" || "$msys" ; then 115 + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 116 + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 117 + 118 + JAVACMD=$( cygpath --unix "$JAVACMD" ) 119 + 120 + # Now convert the arguments - kludge to limit ourselves to /bin/sh 121 + for arg do 122 + if 123 + case $arg in #( 124 + -*) false ;; # don't mess with options #( 125 + /?*) t=${arg#)}; t=/${t%%/*} # looks like a POSIX filepath 126 + [ -e "$t" ] ;; #( 127 + *) false ;; 128 + esac 129 + then 130 + arg=$( cygpath --path --ignore --mixed "$arg" ) 131 + fi 132 + # Roll the args list around exactly as many times as the number of 133 + # temporary files were generated. 134 + set -- "$@" "$arg" 135 + shift 136 + done 137 + fi 138 + 139 + 140 + # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 141 + DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 142 + 143 + # Collect all arguments for the java command: 144 + # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not:// having any whitespace:// * example:// having any whitespace 145 + set -- \ 146 + "-Dorg.gradle.appname=$APP_BASE_NAME" \ 147 + -classpath "$CLASSPATH" \ 148 + org.gradle.wrapper.GradleWrapperMain \ 149 + "$@" 150 + 151 + # Stop when "xargs" is not available. 152 + if ! command -v xargs >/dev/null 2>&1 153 + then 154 + die "xargs is not available" 155 + fi 156 + 157 + # Use "xargs" to parse quoted args. 158 + # 159 + # With -n:// * "arg// "arg one two three" "arg" 160 + # ... would result in:// * "arg" "one two three" "arg" 161 + # 162 + # We need to respect:// * and then eval, for any shell// * The output is handled by the function:// *:// * The output is then read by xargs. 163 + eval "set -- $( 164 + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 165 + xargs -n1 | 166 + sed ' s~[`528"]~\\&~g; ' | 167 + tr '\n' ' ' 168 + )" '"$@"' 169 + 170 + exec "$JAVACMD" "$@"
+86
android/gradlew.bat
··· 1 + @rem 2 + @rem Copyright 2015-2024 the original author or authors. 3 + @rem 4 + @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 + @rem you may not use this file except in compliance with the License. 6 + @rem You may obtain a copy of the License at 7 + @rem 8 + @rem https://www.apache.org/licenses/LICENSE-2.0 9 + @rem 10 + 11 + @if "%DEBUG%"=="" @echo off 12 + @rem ########################################################################## 13 + @rem 14 + @rem Gradle startup script for Windows 15 + @rem 16 + @rem ########################################################################## 17 + 18 + @rem Set local scope for the variables with windows NT shell 19 + if "%OS%"=="Windows_NT" setlocal 20 + 21 + set DIRNAME=%~dp0 22 + if "%DIRNAME%"=="" set DIRNAME=. 23 + @rem This is normally unused 24 + set APP_BASE_NAME=%~n0 25 + set APP_HOME=%DIRNAME% 26 + 27 + @rem Resolve any "." and ".." in APP_HOME to make it shorter. 28 + for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 29 + 30 + @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 + set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 32 + 33 + @rem Find java.exe 34 + if defined JAVA_HOME goto findJavaFromJavaHome 35 + 36 + set JAVA_EXE=java.exe 37 + %JAVA_EXE% -version >NUL 2>&1 38 + if %ERRORLEVEL% equ 0 goto execute 39 + 40 + echo. 1>&2 41 + echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 42 + echo. 1>&2 43 + echo Please set the JAVA_HOME variable in your environment to match the 1>&2 44 + echo location of your Java installation. 1>&2 45 + 46 + goto fail 47 + 48 + :findJavaFromJavaHome 49 + set JAVA_HOME=%JAVA_HOME:"=% 50 + set JAVA_EXE=%JAVA_HOME%/bin/java.exe 51 + 52 + if exist "%JAVA_EXE%" goto execute 53 + 54 + echo. 1>&2 55 + echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 56 + echo. 1>&2 57 + echo Please set the JAVA_HOME variable in your environment to match the 1>&2 58 + echo location of your Java installation. 1>&2 59 + 60 + goto fail 61 + 62 + :execute 63 + @rem Setup the command line 64 + 65 + set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 66 + 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 %* 70 + 71 + :end 72 + @rem End local scope for the variables with windows NT shell 73 + if %ERRORLEVEL% equ 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 + set EXIT_CODE=%ERRORLEVEL% 79 + if %EXIT_CODE% equ 0 set EXIT_CODE=1 80 + if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 81 + exit /b %EXIT_CODE% 82 + 83 + :mainEnd 84 + if "%OS%"=="Windows_NT" endlocal 85 + 86 + :omega
+23
android/settings.gradle.kts
··· 1 + pluginManagement { 2 + repositories { 3 + google { 4 + content { 5 + includeGroupByRegex("com\\.android.*") 6 + includeGroupByRegex("com\\.google.*") 7 + includeGroupByRegex("androidx.*") 8 + } 9 + } 10 + mavenCentral() 11 + gradlePluginPortal() 12 + } 13 + } 14 + dependencyResolutionManagement { 15 + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 16 + repositories { 17 + google() 18 + mavenCentral() 19 + } 20 + } 21 + 22 + rootProject.name = "WasupChucks" 23 + include(":app")