diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index d2eed970..a73000b7 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -30,4 +30,6 @@ dependencies {
implementation(projects.feature.home)
implementation(projects.core.designsystem)
+
+ implementation(projects.widget)
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 243ce8bd..b5a9f67c 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -16,6 +16,17 @@
android:theme="@style/Theme.DroidKnights2023"
tools:targetApi="31">
+
+
+
+
+
+
+
diff --git a/core/designsystem/build.gradle.kts b/core/designsystem/build.gradle.kts
index a6829feb..817da814 100644
--- a/core/designsystem/build.gradle.kts
+++ b/core/designsystem/build.gradle.kts
@@ -13,4 +13,6 @@ dependencies {
implementation(libs.landscapist.bom)
implementation(libs.landscapist.coil)
implementation(libs.landscapist.placeholder)
+
+ implementation(libs.androidx.glance)
}
diff --git a/core/designsystem/src/main/java/com/droidknights/app2023/core/designsystem/theme/Theme.kt b/core/designsystem/src/main/java/com/droidknights/app2023/core/designsystem/theme/Theme.kt
index 1298f9b6..66a10a88 100644
--- a/core/designsystem/src/main/java/com/droidknights/app2023/core/designsystem/theme/Theme.kt
+++ b/core/designsystem/src/main/java/com/droidknights/app2023/core/designsystem/theme/Theme.kt
@@ -13,6 +13,9 @@ import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
+import androidx.glance.GlanceTheme
+import androidx.glance.color.ColorProvider
+import androidx.glance.color.colorProviders
private val DarkColorScheme = darkColorScheme(
primary = White,
@@ -86,7 +89,8 @@ fun KnightsTheme(
SideEffect {
val window = (view.context as Activity).window
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme
- WindowCompat.getInsetsController(window, view).isAppearanceLightNavigationBars = !darkTheme
+ WindowCompat.getInsetsController(window, view).isAppearanceLightNavigationBars =
+ !darkTheme
}
}
@@ -106,3 +110,69 @@ object KnightsTheme {
@Composable
get() = LocalTypography.current
}
+
+private val WidgetColorProviers = colorProviders(
+ primary = ColorProvider(LightColorScheme.primary, DarkColorScheme.primary),
+ onPrimary = ColorProvider(LightColorScheme.onPrimary, DarkColorScheme.onPrimary),
+ primaryContainer = ColorProvider(
+ LightColorScheme.primaryContainer,
+ DarkColorScheme.primaryContainer
+ ),
+ onPrimaryContainer = ColorProvider(
+ LightColorScheme.onPrimaryContainer,
+ DarkColorScheme.onPrimaryContainer
+ ),
+ inversePrimary = ColorProvider(LightColorScheme.inversePrimary, DarkColorScheme.inversePrimary),
+ secondary = ColorProvider(LightColorScheme.secondary, DarkColorScheme.secondary),
+ onSecondary = ColorProvider(LightColorScheme.onSecondary, DarkColorScheme.onSecondary),
+ secondaryContainer = ColorProvider(
+ LightColorScheme.secondaryContainer,
+ DarkColorScheme.secondaryContainer
+ ),
+ onSecondaryContainer = ColorProvider(
+ LightColorScheme.onSecondaryContainer,
+ DarkColorScheme.onSecondaryContainer
+ ),
+ tertiary = ColorProvider(LightColorScheme.tertiary, DarkColorScheme.tertiary),
+ onTertiary = ColorProvider(LightColorScheme.onTertiary, DarkColorScheme.onTertiary),
+ tertiaryContainer = ColorProvider(
+ LightColorScheme.tertiaryContainer,
+ DarkColorScheme.tertiaryContainer
+ ),
+ onTertiaryContainer = ColorProvider(
+ LightColorScheme.onTertiaryContainer,
+ DarkColorScheme.onTertiaryContainer
+ ),
+ error = ColorProvider(LightColorScheme.error, DarkColorScheme.error),
+ onError = ColorProvider(LightColorScheme.onError, DarkColorScheme.onError),
+ errorContainer = ColorProvider(LightColorScheme.errorContainer, DarkColorScheme.errorContainer),
+ onErrorContainer = ColorProvider(
+ LightColorScheme.onErrorContainer,
+ DarkColorScheme.onErrorContainer
+ ),
+ surface = ColorProvider(LightColorScheme.surface, DarkColorScheme.surface),
+ onSurface = ColorProvider(LightColorScheme.onSurface, DarkColorScheme.onSurface),
+ inverseSurface = ColorProvider(LightColorScheme.inverseSurface, DarkColorScheme.inverseSurface),
+ inverseOnSurface = ColorProvider(
+ LightColorScheme.inverseOnSurface,
+ DarkColorScheme.inverseOnSurface
+ ),
+ outline = ColorProvider(LightColorScheme.outline, DarkColorScheme.outline),
+ background = ColorProvider(LightColorScheme.background, DarkColorScheme.background),
+ onBackground = ColorProvider(LightColorScheme.onBackground, DarkColorScheme.onBackground),
+ surfaceVariant = ColorProvider(LightColorScheme.surfaceVariant, DarkColorScheme.surfaceVariant),
+ onSurfaceVariant = ColorProvider(
+ LightColorScheme.onSurfaceVariant,
+ DarkColorScheme.onSurfaceVariant
+ )
+)
+
+@Composable
+fun KnightsGlanceTheme(
+ content: @Composable () -> Unit,
+) {
+ GlanceTheme(
+ colors = WidgetColorProviers,
+ content = content
+ )
+}
diff --git a/feature/main/build.gradle.kts b/feature/main/build.gradle.kts
index c9b47813..72d66d50 100644
--- a/feature/main/build.gradle.kts
+++ b/feature/main/build.gradle.kts
@@ -13,6 +13,8 @@ dependencies {
implementation(projects.feature.session)
implementation(projects.feature.bookmark)
+ implementation(projects.widget)
+
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.activity.compose)
diff --git a/feature/main/src/main/java/com/droidknights/app2023/feature/main/MainActivity.kt b/feature/main/src/main/java/com/droidknights/app2023/feature/main/MainActivity.kt
index 37ed9593..63a1ea10 100644
--- a/feature/main/src/main/java/com/droidknights/app2023/feature/main/MainActivity.kt
+++ b/feature/main/src/main/java/com/droidknights/app2023/feature/main/MainActivity.kt
@@ -4,24 +4,45 @@ import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.core.view.WindowCompat
import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.droidknights.app2023.DroidKnightsWidget.Companion.KEY_SESSION_ID
import com.droidknights.app2023.core.designsystem.theme.KnightsTheme
import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.flow.MutableStateFlow
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val viewModel: MainViewModel by viewModels()
+ private val sessionIdFromWidget: MutableStateFlow = MutableStateFlow(null)
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ intent.getStringExtra(KEY_SESSION_ID)?.let {
+ sessionIdFromWidget.value = it
+ intent.removeExtra(KEY_SESSION_ID)
+ }
+
WindowCompat.setDecorFitsSystemWindows(window, false)
setContent {
val isDarkTheme by viewModel.isDarkTheme.collectAsStateWithLifecycle(false, this)
+
+ val navigator: MainNavigator = rememberMainNavigator()
+ val sessionId = sessionIdFromWidget.collectAsStateWithLifecycle().value
+
+ LaunchedEffect(sessionId) {
+ sessionId?.let {
+ navigator.navigateSessionDetail(it)
+ }
+ }
+
KnightsTheme(darkTheme = isDarkTheme) {
MainScreen(
+ navigator = navigator,
onChangeDarkTheme = { isDarkTheme -> viewModel.updateIsDarkTheme(isDarkTheme) }
)
}
diff --git a/feature/main/src/main/java/com/droidknights/app2023/feature/main/MainViewModel.kt b/feature/main/src/main/java/com/droidknights/app2023/feature/main/MainViewModel.kt
index edb0208d..494c3fd4 100644
--- a/feature/main/src/main/java/com/droidknights/app2023/feature/main/MainViewModel.kt
+++ b/feature/main/src/main/java/com/droidknights/app2023/feature/main/MainViewModel.kt
@@ -1,18 +1,25 @@
package com.droidknights.app2023.feature.main
+import android.app.Application
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.droidknights.app2023.core.data.repository.SettingsRepository
+import com.droidknights.app2023.sendWidgetUpdateCommand
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class MainViewModel @Inject constructor(
+ private val application: Application,
private val settingsRepository: SettingsRepository,
) : ViewModel() {
val isDarkTheme = settingsRepository.getIsDarkTheme()
+ init {
+ sendWidgetUpdateCommand(application)
+ }
+
fun updateIsDarkTheme(isDarkTheme: Boolean) = viewModelScope.launch {
settingsRepository.updateIsDarkTheme(isDarkTheme)
}
diff --git a/feature/session/build.gradle.kts b/feature/session/build.gradle.kts
index 0d8627a0..047f488f 100644
--- a/feature/session/build.gradle.kts
+++ b/feature/session/build.gradle.kts
@@ -8,4 +8,5 @@ android {
dependencies {
implementation(libs.kotlinx.immutable)
+ implementation(projects.widget)
}
diff --git a/feature/session/src/main/java/com/droidknights/app2023/feature/session/SessionDetailScreen.kt b/feature/session/src/main/java/com/droidknights/app2023/feature/session/SessionDetailScreen.kt
index 0f0155e8..202c1683 100644
--- a/feature/session/src/main/java/com/droidknights/app2023/feature/session/SessionDetailScreen.kt
+++ b/feature/session/src/main/java/com/droidknights/app2023/feature/session/SessionDetailScreen.kt
@@ -1,5 +1,6 @@
package com.droidknights.app2023.feature.session
+import android.util.Log
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -29,6 +30,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
@@ -50,6 +52,7 @@ import com.droidknights.app2023.core.model.Room
import com.droidknights.app2023.core.model.Session
import com.droidknights.app2023.core.model.Speaker
import com.droidknights.app2023.core.model.Tag
+import com.droidknights.app2023.sendWidgetUpdateCommand
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.delay
@@ -64,6 +67,14 @@ internal fun SessionDetailScreen(
val scrollState = rememberScrollState()
val sessionUiState by viewModel.sessionUiState.collectAsStateWithLifecycle()
val effect by viewModel.sessionUiEffect.collectAsStateWithLifecycle()
+
+ val context = LocalContext.current
+
+ LaunchedEffect(effect) {
+ if(effect is SessionDetailEffect.ShowToastForBookmarkState) {
+ sendWidgetUpdateCommand(context)
+ }
+ }
Column(
modifier = Modifier
.fillMaxSize()
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 55a6a3ee..b8a23c27 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -41,6 +41,8 @@ androidxDatastore = "1.0.0"
ossLicenses = "17.0.1"
ossLicensesPlugin = "0.10.6"
+androidxGlance = "1.0.0-beta01"
+
[libraries]
android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" }
android-desugarJdkLibs = { group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "androidDesugarJdkLibs" }
@@ -83,7 +85,7 @@ landscapist-coil = { group = "com.github.skydoves", name = "landscapist-coil" }
landscapist-placeholder = { group = "com.github.skydoves", name = "landscapist-placeholder" }
landscapist-animation = { group = "com.github.skydoves", name = "landscapist-animation" }
-compose-shimmer = { group = "com.valentinilk.shimmer", name = "compose-shimmer", version.ref = "composeShimmer"}
+compose-shimmer = { group = "com.valentinilk.shimmer", name = "compose-shimmer", version.ref = "composeShimmer" }
junit4 = { group = "junit", name = "junit", version.ref = "junit4" }
junit-vintage-engine = { group = "org.junit.vintage", name = "junit-vintage-engine", version.ref = "junitVintageEngine" }
@@ -110,6 +112,9 @@ verify-detektPlugin = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plug
androidx-datastore = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "androidxDatastore" }
oss-licenses-plugin = { group = "com.google.android.gms", name = "oss-licenses-plugin", version.ref = "ossLicensesPlugin" }
+androidx-glance = { group = "androidx.glance", name = "glance", version.ref = "androidxGlance" }
+androidx-glance-appwidget = { group = "androidx.glance", name = "glance-appwidget", version.ref = "androidxGlance" }
+
[bundles]
[plugins]
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 232f5f93..ead104c5 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -34,4 +34,6 @@ include(
":feature:setting",
":feature:contributor",
":feature:bookmark",
+
+ ":widget"
)
diff --git a/widget/.gitignore b/widget/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/widget/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/widget/build.gradle.kts b/widget/build.gradle.kts
new file mode 100644
index 00000000..897f9247
--- /dev/null
+++ b/widget/build.gradle.kts
@@ -0,0 +1,18 @@
+plugins {
+ id("droidknights.android.library")
+ id("droidknights.android.compose")
+}
+
+android {
+ namespace = "com.droidknights.app2023.widget"
+}
+
+dependencies {
+ implementation(libs.androidx.glance)
+ implementation(libs.androidx.glance.appwidget)
+
+ implementation(projects.core.designsystem)
+
+ implementation(projects.core.domain)
+ implementation(project(mapOf("path" to ":core:model")))
+}
\ No newline at end of file
diff --git a/widget/src/main/kotlin/com/droidknights/app2023/Action.kt b/widget/src/main/kotlin/com/droidknights/app2023/Action.kt
new file mode 100644
index 00000000..98f87b2c
--- /dev/null
+++ b/widget/src/main/kotlin/com/droidknights/app2023/Action.kt
@@ -0,0 +1,24 @@
+package com.droidknights.app2023
+
+import android.content.Context
+import android.content.Intent
+import androidx.glance.action.Action
+import androidx.glance.appwidget.action.actionStartActivity
+
+fun actionStartActivityWithSessionId(context: Context, sessionId: String): Action =
+ actionStartActivity(
+ Intent(
+ context.packageManager.getLaunchIntentForPackage(
+ context.packageName
+ )
+ ).putExtra(DroidKnightsWidget.KEY_SESSION_ID, sessionId)
+ .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
+ )
+
+fun actionLaunchIntentForPackage(context: Context): Action = actionStartActivity(
+ Intent(
+ context.packageManager.getLaunchIntentForPackage(
+ context.packageName
+ )
+ )
+)
\ No newline at end of file
diff --git a/widget/src/main/kotlin/com/droidknights/app2023/Command.kt b/widget/src/main/kotlin/com/droidknights/app2023/Command.kt
new file mode 100644
index 00000000..e53a4b98
--- /dev/null
+++ b/widget/src/main/kotlin/com/droidknights/app2023/Command.kt
@@ -0,0 +1,16 @@
+package com.droidknights.app2023
+
+import android.appwidget.AppWidgetManager
+import android.content.Context
+import android.content.Intent
+
+fun sendWidgetUpdateCommand(context: Context) {
+ context.sendBroadcast(
+ Intent(
+ context,
+ DroidKnightsWidgetReceiver::class.java
+ ).setAction(
+ AppWidgetManager.ACTION_APPWIDGET_UPDATE
+ )
+ )
+}
\ No newline at end of file
diff --git a/widget/src/main/kotlin/com/droidknights/app2023/DroidKnightsWidget.kt b/widget/src/main/kotlin/com/droidknights/app2023/DroidKnightsWidget.kt
new file mode 100644
index 00000000..cb8d8a79
--- /dev/null
+++ b/widget/src/main/kotlin/com/droidknights/app2023/DroidKnightsWidget.kt
@@ -0,0 +1,75 @@
+package com.droidknights.app2023
+
+import android.content.Context
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.unit.dp
+import androidx.datastore.preferences.core.stringSetPreferencesKey
+import androidx.glance.GlanceId
+import androidx.glance.GlanceModifier
+import androidx.glance.GlanceTheme
+import androidx.glance.appwidget.GlanceAppWidget
+import androidx.glance.appwidget.lazy.LazyColumn
+import androidx.glance.appwidget.lazy.items
+import androidx.glance.appwidget.provideContent
+import androidx.glance.background
+import androidx.glance.currentState
+import androidx.glance.layout.Alignment
+import androidx.glance.layout.Column
+import androidx.glance.layout.Spacer
+import androidx.glance.layout.fillMaxSize
+import androidx.glance.layout.height
+import androidx.glance.layout.padding
+import com.droidknights.app2023.DroidKnightsWidgetReceiver.Companion.KEY_SESSION_IDS
+import com.droidknights.app2023.core.designsystem.theme.KnightsGlanceTheme
+import com.droidknights.app2023.core.model.Session
+import com.droidknights.app2023.di.WidgetModule
+import dagger.hilt.EntryPoints
+
+class DroidKnightsWidget : GlanceAppWidget() {
+
+ companion object {
+ const val KEY_SESSION_ID = "KEY_SESSION_ID"
+ }
+
+ override suspend fun provideGlance(context: Context, id: GlanceId) {
+ val widgetModule: WidgetModule = EntryPoints.get(
+ context.applicationContext,
+ WidgetModule::class.java
+ )
+
+ provideContent {
+ KnightsGlanceTheme {
+ val state = currentState(stringSetPreferencesKey(KEY_SESSION_IDS))
+ var list: List by remember(state) { mutableStateOf(listOf()) }
+
+ LaunchedEffect(state) {
+ list = arrayListOf().apply {
+ state?.forEach {
+ this.add(widgetModule.getSessionUseCase().invoke(it))
+ }
+ }
+ }
+
+ Column(
+ modifier = GlanceModifier
+ .fillMaxSize()
+ .padding(start = 16.dp, top = 16.dp, bottom = 16.dp)
+ .background(GlanceTheme.colors.surface),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ WidgetTitle()
+ Spacer(modifier = GlanceModifier.height(16.dp))
+ LazyColumn {
+ items(list) {
+ WidgetSessionCard(it)
+ }
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/widget/src/main/kotlin/com/droidknights/app2023/DroidKnightsWidgetReceiver.kt b/widget/src/main/kotlin/com/droidknights/app2023/DroidKnightsWidgetReceiver.kt
new file mode 100644
index 00000000..56138a57
--- /dev/null
+++ b/widget/src/main/kotlin/com/droidknights/app2023/DroidKnightsWidgetReceiver.kt
@@ -0,0 +1,76 @@
+package com.droidknights.app2023
+
+import android.appwidget.AppWidgetManager
+import android.content.Context
+import android.content.Intent
+import androidx.datastore.preferences.core.stringSetPreferencesKey
+import androidx.glance.appwidget.GlanceAppWidget
+import androidx.glance.appwidget.GlanceAppWidgetManager
+import androidx.glance.appwidget.GlanceAppWidgetReceiver
+import androidx.glance.appwidget.state.updateAppWidgetState
+import androidx.glance.appwidget.updateAll
+import androidx.glance.state.PreferencesGlanceStateDefinition
+import com.droidknights.app2023.di.WidgetModule
+import dagger.hilt.EntryPoints
+import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+@AndroidEntryPoint
+class DroidKnightsWidgetReceiver : GlanceAppWidgetReceiver() {
+
+ companion object {
+ const val KEY_SESSION_IDS = "SESSION_IDS"
+ }
+
+ override val glanceAppWidget: GlanceAppWidget = DroidKnightsWidget()
+
+ override fun onUpdate(
+ context: Context,
+ appWidgetManager: AppWidgetManager,
+ appWidgetIds: IntArray
+ ) {
+ getBookmarkedSessionAndUpdateWidget(context, glanceAppWidget)
+ super.onUpdate(context, appWidgetManager, appWidgetIds)
+ }
+
+ override fun onReceive(context: Context, intent: Intent) {
+ getBookmarkedSessionAndUpdateWidget(context, glanceAppWidget)
+ super.onReceive(context, intent)
+ }
+}
+
+private fun getBookmarkedSessionAndUpdateWidget(
+ context: Context,
+ glanceAppWidget: GlanceAppWidget
+) {
+ val widgetModule: WidgetModule = EntryPoints.get(
+ context.applicationContext,
+ WidgetModule::class.java
+ )
+
+ CoroutineScope(Dispatchers.IO).launch {
+ val glanceIds = GlanceAppWidgetManager(context).getGlanceIds(DroidKnightsWidget::class.java)
+ widgetModule.getBookmarkedSessionsUseCase().invoke().collect { list ->
+ glanceIds.forEach { glanceId ->
+ updateAppWidgetState(
+ context = context,
+ definition = PreferencesGlanceStateDefinition,
+ glanceId = glanceId
+ ) {
+ val set: Set = mutableSetOf().apply {
+ list.forEach { session ->
+ this.add(session.id)
+ }
+ }
+ it.toMutablePreferences().apply {
+ this[stringSetPreferencesKey(DroidKnightsWidgetReceiver.KEY_SESSION_IDS)] =
+ set
+ }
+ }
+ }
+ glanceAppWidget.updateAll(context)
+ }
+ }
+}
\ No newline at end of file
diff --git a/widget/src/main/kotlin/com/droidknights/app2023/WidgetSessionCard.kt b/widget/src/main/kotlin/com/droidknights/app2023/WidgetSessionCard.kt
new file mode 100644
index 00000000..80c54c5c
--- /dev/null
+++ b/widget/src/main/kotlin/com/droidknights/app2023/WidgetSessionCard.kt
@@ -0,0 +1,67 @@
+package com.droidknights.app2023
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.glance.GlanceModifier
+import androidx.glance.GlanceTheme
+import androidx.glance.LocalContext
+import androidx.glance.action.clickable
+import androidx.glance.appwidget.cornerRadius
+import androidx.glance.background
+import androidx.glance.layout.Box
+import androidx.glance.layout.Column
+import androidx.glance.layout.Row
+import androidx.glance.layout.Spacer
+import androidx.glance.layout.fillMaxWidth
+import androidx.glance.layout.padding
+import androidx.glance.layout.width
+import androidx.glance.text.Text
+import androidx.glance.text.TextDefaults
+import com.droidknights.app2023.core.model.Session
+import kotlinx.datetime.toJavaLocalDateTime
+
+@Composable
+fun WidgetSessionCard(session: Session) {
+ val context = LocalContext.current
+
+ Box(modifier = GlanceModifier.padding(bottom = 16.dp, end = 16.dp)) {
+ Column(
+ modifier = GlanceModifier.padding(16.dp).fillMaxWidth()
+ .cornerRadius(12.dp).background(GlanceTheme.colors.tertiaryContainer).clickable(
+ actionStartActivityWithSessionId(context, session.id)
+ )
+ ) {
+ Text(
+ session.title,
+ style = TextDefaults.defaultTextStyle.copy(
+ fontSize = 16.sp,
+ color = GlanceTheme.colors.onTertiaryContainer
+ )
+ )
+ Row {
+ Text(
+ session.toTimeString(),
+ style = TextDefaults.defaultTextStyle.copy(
+ fontSize = 14.sp,
+ color = GlanceTheme.colors.onTertiaryContainer
+ )
+ )
+ Spacer(modifier = GlanceModifier.width(4.dp))
+ Text(
+ // FIXME : 2명 이상 발표자 있는 case에 대해 정상 동작하도록 수정 필요
+ session.speakers.first().name,
+ style = TextDefaults.defaultTextStyle.copy(
+ fontSize = 14.sp,
+ color = GlanceTheme.colors.onTertiaryContainer
+ )
+ )
+ }
+ }
+ }
+}
+
+private fun Session.toTimeString(): String =
+ "${startTime.toJavaLocalDateTime().toLocalTime()}" +
+ " ~ " +
+ "${endTime.toJavaLocalDateTime().toLocalTime()}"
\ No newline at end of file
diff --git a/widget/src/main/kotlin/com/droidknights/app2023/WidgetTitle.kt b/widget/src/main/kotlin/com/droidknights/app2023/WidgetTitle.kt
new file mode 100644
index 00000000..24ca2ad4
--- /dev/null
+++ b/widget/src/main/kotlin/com/droidknights/app2023/WidgetTitle.kt
@@ -0,0 +1,27 @@
+package com.droidknights.app2023
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.unit.sp
+import androidx.glance.GlanceModifier
+import androidx.glance.GlanceTheme
+import androidx.glance.LocalContext
+import androidx.glance.action.clickable
+import androidx.glance.text.Text
+import androidx.glance.text.TextDefaults
+import com.droidknights.app2023.widget.R
+
+@Composable
+fun WidgetTitle() {
+ val context = LocalContext.current
+
+ Text(
+ context.getString(R.string.widget_title),
+ style = TextDefaults.defaultTextStyle.copy(
+ fontSize = 24.sp,
+ color = GlanceTheme.colors.onSurface
+ ),
+ modifier = GlanceModifier.clickable(
+ actionLaunchIntentForPackage(context)
+ )
+ )
+}
\ No newline at end of file
diff --git a/widget/src/main/kotlin/com/droidknights/app2023/di/WidgetModule.kt b/widget/src/main/kotlin/com/droidknights/app2023/di/WidgetModule.kt
new file mode 100644
index 00000000..5a094033
--- /dev/null
+++ b/widget/src/main/kotlin/com/droidknights/app2023/di/WidgetModule.kt
@@ -0,0 +1,15 @@
+package com.droidknights.app2023.di
+
+import com.droidknights.app2023.core.domain.usecase.GetBookmarkedSessionsUseCase
+import com.droidknights.app2023.core.domain.usecase.GetSessionUseCase
+import dagger.hilt.EntryPoint
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+
+@EntryPoint
+@InstallIn(SingletonComponent::class)
+interface WidgetModule {
+ fun getBookmarkedSessionsUseCase(): GetBookmarkedSessionsUseCase
+
+ fun getSessionUseCase(): GetSessionUseCase
+}
\ No newline at end of file
diff --git a/widget/src/main/res/values/strings.xml b/widget/src/main/res/values/strings.xml
new file mode 100644
index 00000000..723e9471
--- /dev/null
+++ b/widget/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+
+ Droid Knights 2023
+
\ No newline at end of file
diff --git a/widget/src/main/res/xml-v31/widget_info.xml b/widget/src/main/res/xml-v31/widget_info.xml
new file mode 100644
index 00000000..702b8d42
--- /dev/null
+++ b/widget/src/main/res/xml-v31/widget_info.xml
@@ -0,0 +1,9 @@
+
+
\ No newline at end of file
diff --git a/widget/src/main/res/xml/widget_info.xml b/widget/src/main/res/xml/widget_info.xml
new file mode 100644
index 00000000..791111e2
--- /dev/null
+++ b/widget/src/main/res/xml/widget_info.xml
@@ -0,0 +1,9 @@
+
+
\ No newline at end of file