diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 3ed7471c..66c00162 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -122,6 +122,7 @@ dependencies {
implementation(projects.core.data)
implementation(projects.core.sync)
implementation(projects.core.opml)
+ implementation(projects.widget)
implementation(projects.ui.resources)
implementation(projects.ui.preview)
implementation(projects.ui.designSystem)
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 1bdb40c2..89bcb5b9 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -29,6 +29,7 @@ compose-bom = "2024.06.00"
compose-bom-alpha = "2024.07.00-alpha01"
compose-htmlconverter = "0.9.5"
kmpalette = "3.1.0"
+glance = "1.1.0"
lyricist = "1.7.0"
navigation = "2.8.0-beta05"
molecule = "2.0.0"
@@ -77,6 +78,9 @@ lyricist = { group = "cafe.adriel.lyricist", name = "lyricist", version.ref = "l
lyricist-processor = { group = "cafe.adriel.lyricist", name = "lyricist-processor", version.ref = "lyricist" }
ui = { group = "androidx.compose.ui", name = "ui" }
ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
+glance = { module = "androidx.glance:glance", version.ref = "glance" }
+glance-appwidget = { module = "androidx.glance:glance-appwidget", version.ref = "glance" }
+glance-material3 = { module = "androidx.glance:glance-material3", version.ref = "glance" }
ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
@@ -179,6 +183,11 @@ compose = [
"aboutlibraries-m3",
"compose-htmlconverter"
]
+glance = [
+ "glance",
+ "glance-appwidget",
+ "glance-material3"
+]
navigation = [
"navigation",
"hilt-navigation-compose"
diff --git a/settings.gradle.kts b/settings.gradle.kts
index fd98ffa9..5a0f3184 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -31,3 +31,4 @@ include(":core:opml")
include(":ui:resources")
include(":ui:preview")
include(":ui:design-system")
+include(":widget")
diff --git a/widget/build.gradle.kts b/widget/build.gradle.kts
new file mode 100644
index 00000000..14397a3e
--- /dev/null
+++ b/widget/build.gradle.kts
@@ -0,0 +1,24 @@
+plugins {
+ alias(libs.plugins.podcaster.compose.android.lib)
+ alias(libs.plugins.hilt)
+ alias(libs.plugins.ksp)
+}
+
+android {
+ namespace = "com.mr3y.podcaster.widget"
+}
+
+dependencies {
+ ksp(libs.hilt.compiler)
+ implementation(libs.hilt.runtime)
+
+ implementation(libs.bundles.glance)
+ implementation(libs.material3)
+ implementation(libs.coil.mp)
+
+ implementation(projects.core.data)
+ implementation(projects.core.logger)
+ implementation(projects.core.model)
+ implementation(projects.ui.resources)
+ implementation(projects.ui.designSystem)
+}
\ No newline at end of file
diff --git a/widget/src/main/AndroidManifest.xml b/widget/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..06e6f091
--- /dev/null
+++ b/widget/src/main/AndroidManifest.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/widget/src/main/kotlin/com/mr3y/podcaster/widget/Color.kt b/widget/src/main/kotlin/com/mr3y/podcaster/widget/Color.kt
new file mode 100644
index 00000000..f4831c0d
--- /dev/null
+++ b/widget/src/main/kotlin/com/mr3y/podcaster/widget/Color.kt
@@ -0,0 +1,126 @@
+package com.mr3y.podcaster.widget
+
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.lightColorScheme
+import com.mr3y.podcaster.ui.theme.md_theme_dark_background
+import com.mr3y.podcaster.ui.theme.md_theme_dark_error
+import com.mr3y.podcaster.ui.theme.md_theme_dark_errorContainer
+import com.mr3y.podcaster.ui.theme.md_theme_dark_inverseOnSurface
+import com.mr3y.podcaster.ui.theme.md_theme_dark_inversePrimary
+import com.mr3y.podcaster.ui.theme.md_theme_dark_inverseSurface
+import com.mr3y.podcaster.ui.theme.md_theme_dark_onBackground
+import com.mr3y.podcaster.ui.theme.md_theme_dark_onError
+import com.mr3y.podcaster.ui.theme.md_theme_dark_onErrorContainer
+import com.mr3y.podcaster.ui.theme.md_theme_dark_onPrimary
+import com.mr3y.podcaster.ui.theme.md_theme_dark_onPrimaryContainer
+import com.mr3y.podcaster.ui.theme.md_theme_dark_onSecondary
+import com.mr3y.podcaster.ui.theme.md_theme_dark_onSecondaryContainer
+import com.mr3y.podcaster.ui.theme.md_theme_dark_onSurface
+import com.mr3y.podcaster.ui.theme.md_theme_dark_onSurfaceVariant
+import com.mr3y.podcaster.ui.theme.md_theme_dark_onTertiary
+import com.mr3y.podcaster.ui.theme.md_theme_dark_onTertiaryContainer
+import com.mr3y.podcaster.ui.theme.md_theme_dark_outline
+import com.mr3y.podcaster.ui.theme.md_theme_dark_outlineVariant
+import com.mr3y.podcaster.ui.theme.md_theme_dark_primary
+import com.mr3y.podcaster.ui.theme.md_theme_dark_primaryContainer
+import com.mr3y.podcaster.ui.theme.md_theme_dark_scrim
+import com.mr3y.podcaster.ui.theme.md_theme_dark_secondary
+import com.mr3y.podcaster.ui.theme.md_theme_dark_secondaryContainer
+import com.mr3y.podcaster.ui.theme.md_theme_dark_surface
+import com.mr3y.podcaster.ui.theme.md_theme_dark_surfaceTint
+import com.mr3y.podcaster.ui.theme.md_theme_dark_surfaceVariant
+import com.mr3y.podcaster.ui.theme.md_theme_dark_tertiary
+import com.mr3y.podcaster.ui.theme.md_theme_dark_tertiaryContainer
+import com.mr3y.podcaster.ui.theme.md_theme_light_background
+import com.mr3y.podcaster.ui.theme.md_theme_light_error
+import com.mr3y.podcaster.ui.theme.md_theme_light_errorContainer
+import com.mr3y.podcaster.ui.theme.md_theme_light_inverseOnSurface
+import com.mr3y.podcaster.ui.theme.md_theme_light_inversePrimary
+import com.mr3y.podcaster.ui.theme.md_theme_light_inverseSurface
+import com.mr3y.podcaster.ui.theme.md_theme_light_onBackground
+import com.mr3y.podcaster.ui.theme.md_theme_light_onError
+import com.mr3y.podcaster.ui.theme.md_theme_light_onErrorContainer
+import com.mr3y.podcaster.ui.theme.md_theme_light_onPrimary
+import com.mr3y.podcaster.ui.theme.md_theme_light_onPrimaryContainer
+import com.mr3y.podcaster.ui.theme.md_theme_light_onSecondary
+import com.mr3y.podcaster.ui.theme.md_theme_light_onSecondaryContainer
+import com.mr3y.podcaster.ui.theme.md_theme_light_onSurface
+import com.mr3y.podcaster.ui.theme.md_theme_light_onSurfaceVariant
+import com.mr3y.podcaster.ui.theme.md_theme_light_onTertiary
+import com.mr3y.podcaster.ui.theme.md_theme_light_onTertiaryContainer
+import com.mr3y.podcaster.ui.theme.md_theme_light_outline
+import com.mr3y.podcaster.ui.theme.md_theme_light_outlineVariant
+import com.mr3y.podcaster.ui.theme.md_theme_light_primary
+import com.mr3y.podcaster.ui.theme.md_theme_light_primaryContainer
+import com.mr3y.podcaster.ui.theme.md_theme_light_scrim
+import com.mr3y.podcaster.ui.theme.md_theme_light_secondary
+import com.mr3y.podcaster.ui.theme.md_theme_light_secondaryContainer
+import com.mr3y.podcaster.ui.theme.md_theme_light_surface
+import com.mr3y.podcaster.ui.theme.md_theme_light_surfaceTint
+import com.mr3y.podcaster.ui.theme.md_theme_light_surfaceVariant
+import com.mr3y.podcaster.ui.theme.md_theme_light_tertiary
+import com.mr3y.podcaster.ui.theme.md_theme_light_tertiaryContainer
+
+internal val LightColors = lightColorScheme(
+ primary = md_theme_light_primary,
+ onPrimary = md_theme_light_onPrimary,
+ primaryContainer = md_theme_light_primaryContainer,
+ onPrimaryContainer = md_theme_light_onPrimaryContainer,
+ secondary = md_theme_light_secondary,
+ onSecondary = md_theme_light_onSecondary,
+ secondaryContainer = md_theme_light_secondaryContainer,
+ onSecondaryContainer = md_theme_light_onSecondaryContainer,
+ tertiary = md_theme_light_tertiary,
+ onTertiary = md_theme_light_onTertiary,
+ tertiaryContainer = md_theme_light_tertiaryContainer,
+ onTertiaryContainer = md_theme_light_onTertiaryContainer,
+ error = md_theme_light_error,
+ errorContainer = md_theme_light_errorContainer,
+ onError = md_theme_light_onError,
+ onErrorContainer = md_theme_light_onErrorContainer,
+ background = md_theme_light_background,
+ onBackground = md_theme_light_onBackground,
+ surface = md_theme_light_surface,
+ onSurface = md_theme_light_onSurface,
+ surfaceVariant = md_theme_light_surfaceVariant,
+ onSurfaceVariant = md_theme_light_onSurfaceVariant,
+ outline = md_theme_light_outline,
+ inverseOnSurface = md_theme_light_inverseOnSurface,
+ inverseSurface = md_theme_light_inverseSurface,
+ inversePrimary = md_theme_light_inversePrimary,
+ surfaceTint = md_theme_light_surfaceTint,
+ outlineVariant = md_theme_light_outlineVariant,
+ scrim = md_theme_light_scrim,
+)
+
+internal val DarkColors = darkColorScheme(
+ primary = md_theme_dark_primary,
+ onPrimary = md_theme_dark_onPrimary,
+ primaryContainer = md_theme_dark_primaryContainer,
+ onPrimaryContainer = md_theme_dark_onPrimaryContainer,
+ secondary = md_theme_dark_secondary,
+ onSecondary = md_theme_dark_onSecondary,
+ secondaryContainer = md_theme_dark_secondaryContainer,
+ onSecondaryContainer = md_theme_dark_onSecondaryContainer,
+ tertiary = md_theme_dark_tertiary,
+ onTertiary = md_theme_dark_onTertiary,
+ tertiaryContainer = md_theme_dark_tertiaryContainer,
+ onTertiaryContainer = md_theme_dark_onTertiaryContainer,
+ error = md_theme_dark_error,
+ errorContainer = md_theme_dark_errorContainer,
+ onError = md_theme_dark_onError,
+ onErrorContainer = md_theme_dark_onErrorContainer,
+ background = md_theme_dark_background,
+ onBackground = md_theme_dark_onBackground,
+ surface = md_theme_dark_surface,
+ onSurface = md_theme_dark_onSurface,
+ surfaceVariant = md_theme_dark_surfaceVariant,
+ onSurfaceVariant = md_theme_dark_onSurfaceVariant,
+ outline = md_theme_dark_outline,
+ inverseOnSurface = md_theme_dark_inverseOnSurface,
+ inverseSurface = md_theme_dark_inverseSurface,
+ inversePrimary = md_theme_dark_inversePrimary,
+ surfaceTint = md_theme_dark_surfaceTint,
+ outlineVariant = md_theme_dark_outlineVariant,
+ scrim = md_theme_dark_scrim,
+)
diff --git a/widget/src/main/kotlin/com/mr3y/podcaster/widget/PodcasterAppWidget.kt b/widget/src/main/kotlin/com/mr3y/podcaster/widget/PodcasterAppWidget.kt
new file mode 100644
index 00000000..de32207d
--- /dev/null
+++ b/widget/src/main/kotlin/com/mr3y/podcaster/widget/PodcasterAppWidget.kt
@@ -0,0 +1,315 @@
+package com.mr3y.podcaster.widget
+
+import android.content.Context
+import android.os.Build
+import androidx.activity.ComponentActivity
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+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.DpSize
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.glance.GlanceId
+import androidx.glance.GlanceModifier
+import androidx.glance.GlanceTheme
+import androidx.glance.Image
+import androidx.glance.ImageProvider
+import androidx.glance.LocalContext
+import androidx.glance.LocalSize
+import androidx.glance.action.actionStartActivity
+import androidx.glance.action.clickable
+import androidx.glance.appwidget.GlanceAppWidget
+import androidx.glance.appwidget.SizeMode
+import androidx.glance.appwidget.components.CircleIconButton
+import androidx.glance.appwidget.components.Scaffold
+import androidx.glance.appwidget.components.SquareIconButton
+import androidx.glance.appwidget.cornerRadius
+import androidx.glance.appwidget.provideContent
+import androidx.glance.background
+import androidx.glance.layout.Alignment
+import androidx.glance.layout.Box
+import androidx.glance.layout.Column
+import androidx.glance.layout.ContentScale
+import androidx.glance.layout.Row
+import androidx.glance.layout.Spacer
+import androidx.glance.layout.fillMaxHeight
+import androidx.glance.layout.fillMaxSize
+import androidx.glance.layout.fillMaxWidth
+import androidx.glance.layout.padding
+import androidx.glance.layout.width
+import androidx.glance.material3.ColorProviders
+import androidx.glance.text.Text
+import androidx.glance.text.TextStyle
+import coil3.Bitmap
+import coil3.BitmapImage
+import coil3.Image
+import coil3.ImageLoader
+import coil3.annotation.ExperimentalCoilApi
+import coil3.request.ErrorResult
+import coil3.request.ImageRequest
+import coil3.size.Scale
+import com.mr3y.podcaster.core.data.PodcastsRepository
+import com.mr3y.podcaster.core.logger.Logger
+import com.mr3y.podcaster.core.model.CurrentlyPlayingEpisode
+import com.mr3y.podcaster.core.model.PlayingStatus
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+class PodcasterAppWidget(
+ private val podcastsRepository: PodcastsRepository,
+ private val logger: Logger
+) : GlanceAppWidget() {
+
+ override val sizeMode: SizeMode = SizeMode.Responsive(setOf(Small, Normal))
+
+ enum class SizeBucket { Invalid, Narrow, Normal }
+
+ override suspend fun provideGlance(context: Context, id: GlanceId) {
+
+ provideContent {
+ val currentlyPlayingEpisode by podcastsRepository.getCurrentlyPlayingEpisode().collectAsState(initial = null)
+ val sizeBucket = calculateSizeBucket()
+
+ GlanceTheme(
+ colors = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
+ GlanceTheme.colors
+ else
+ ColorProviders(light = LightColors, dark = DarkColors)
+ ) {
+ when (sizeBucket) {
+ SizeBucket.Invalid -> InvalidSizeUI()
+ SizeBucket.Narrow -> {
+ WidgetShallowSize(
+ playingStatus = currentlyPlayingEpisode?.playingStatus,
+ onPlayClick = { },
+ onPauseClick = { },
+ modifier = GlanceModifier.fillMaxSize()
+ )
+ }
+ SizeBucket.Normal -> {
+ WidgetNormalSize(
+ activeEpisode = currentlyPlayingEpisode,
+ modifier = GlanceModifier.fillMaxSize()
+ )
+ }
+ }
+ }
+ }
+ }
+
+ @Composable
+ private fun WidgetNormalSize(
+ activeEpisode: CurrentlyPlayingEpisode?,
+ modifier: GlanceModifier = GlanceModifier
+ ) {
+ val context = LocalContext.current.applicationContext
+ Scaffold(
+ backgroundColor = GlanceTheme.colors.surface,
+ modifier = modifier.clickable {
+ /*val launcherIntent = context.packageManager.getLaunchIntentForPackage("com.mr3y.podcaster")
+ if (launcherIntent != null) {
+ context.startActivity(launcherIntent)
+ }*/
+ actionStartActivity()
+ }
+ ) {
+ if (activeEpisode == null) {
+ Box(
+ modifier = GlanceModifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ text = context.getString(R.string.no_episode_playing),
+ style = TextStyle(fontSize = 16.sp, color = GlanceTheme.colors.onSurface),
+ )
+ }
+ } else {
+ Row(
+ modifier = GlanceModifier.padding(vertical = 8.dp).fillMaxSize(),
+ ) {
+ AsyncImage(
+ model = activeEpisode.episode.artworkUrl,
+ contentDescription = null,
+ modifier = GlanceModifier.fillMaxHeight()
+ )
+
+ Spacer(modifier = GlanceModifier.width(8.dp))
+
+ Column(
+ modifier = GlanceModifier.defaultWeight()
+ ) {
+ Text(
+ text = activeEpisode.episode.title,
+ style = TextStyle(fontSize = 16.sp, color = GlanceTheme.colors.onSurface),
+ maxLines = 3,
+ modifier = GlanceModifier.defaultWeight(),
+ )
+ Text(
+ text = activeEpisode.episode.podcastTitle ?: "",
+ style = TextStyle(fontSize = 14.sp, color = GlanceTheme.colors.onSurfaceVariant),
+ maxLines = 2,
+ modifier = GlanceModifier.defaultWeight(),
+ )
+
+ ControlButtons(
+ playingStatus = activeEpisode.playingStatus,
+ onPlayClick = { },
+ onPauseClick = {},
+ onGoToPreviousClick = {},
+ onGoToNextClick = {},
+ modifier = GlanceModifier.fillMaxWidth(),
+ )
+ }
+ }
+ }
+ }
+ }
+
+ @Composable
+ private fun WidgetShallowSize(
+ playingStatus: PlayingStatus?,
+ onPlayClick: () -> Unit,
+ onPauseClick: () -> Unit,
+ modifier: GlanceModifier = GlanceModifier,
+ ) {
+ Scaffold(
+ backgroundColor = GlanceTheme.colors.surface,
+ modifier = modifier.clickable {
+ actionStartActivity()
+ }
+ ) {
+ val context = LocalContext.current
+ if (playingStatus == null) {
+ Box(
+ modifier = GlanceModifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ text = context.getString(R.string.no_episode_playing),
+ style = TextStyle(fontSize = 16.sp, color = GlanceTheme.colors.onSurface),
+ )
+ }
+ } else {
+ val icon = if (playingStatus == PlayingStatus.Paused) R.drawable.outline_play_arrow_24 else R.drawable.outline_pause_24
+ SquareIconButton(
+ imageProvider = ImageProvider(icon),
+ contentDescription = null,
+ onClick = if (playingStatus == PlayingStatus.Paused) onPlayClick else onPauseClick
+ )
+ }
+ }
+ }
+
+ @OptIn(ExperimentalCoilApi::class)
+ @Composable
+ private fun AsyncImage(
+ model: Any,
+ contentDescription: String?,
+ modifier: GlanceModifier = GlanceModifier
+ ) {
+ var bitmap by remember { mutableStateOf(null) }
+ val context = LocalContext.current
+
+ LaunchedEffect(key1 = model) {
+ val request = ImageRequest.Builder(context)
+ .data(model)
+ .size(200, 200)
+ .scale(Scale.FILL)
+ .target { image: Image ->
+ bitmap = (image as BitmapImage).bitmap
+ }
+ .build()
+
+ launch(Dispatchers.IO) {
+ val result = ImageLoader(context).execute(request)
+ if (result is ErrorResult) {
+ val t = result.throwable
+ logger.e(t, tag = TAG) { "Image Request Error:" }
+ }
+ }
+ }
+
+ bitmap?.let {
+ Image(
+ provider = ImageProvider(it),
+ contentDescription = contentDescription,
+ contentScale = ContentScale.FillBounds,
+ modifier = modifier.cornerRadius(12.dp),
+ )
+ }
+ }
+
+ @Composable
+ private fun ControlButtons(
+ playingStatus: PlayingStatus,
+ onPlayClick: () -> Unit,
+ onPauseClick: () -> Unit,
+ onGoToPreviousClick: () -> Unit,
+ onGoToNextClick: () -> Unit,
+ modifier: GlanceModifier = GlanceModifier
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = modifier
+ ) {
+ CircleIconButton(
+ imageProvider = ImageProvider(R.drawable.outline_skip_prev_24),
+ contentDescription = null,
+ onClick = onGoToPreviousClick
+ )
+
+ Spacer(modifier = GlanceModifier.defaultWeight())
+
+ val icon = if (playingStatus == PlayingStatus.Paused) R.drawable.outline_play_arrow_24 else R.drawable.outline_pause_24
+ CircleIconButton(
+ imageProvider = ImageProvider(icon),
+ contentDescription = null,
+ onClick = if (playingStatus == PlayingStatus.Paused) onPlayClick else onPauseClick
+ )
+
+ Spacer(modifier = GlanceModifier.defaultWeight())
+
+ CircleIconButton(
+ imageProvider = ImageProvider(R.drawable.outline_skip_next_24),
+ contentDescription = null,
+ onClick = onGoToNextClick
+ )
+ }
+ }
+
+ @Composable
+ private fun InvalidSizeUI() {
+ Box(
+ modifier = GlanceModifier.fillMaxSize().background(GlanceTheme.colors.surface),
+ contentAlignment = Alignment.Center
+ ) {
+ val context = LocalContext.current
+ Text(
+ text = context.getString(R.string.invalid_size),
+ style = TextStyle(fontSize = 14.sp, color = GlanceTheme.colors.onSurface),
+ )
+ }
+ }
+
+ @Composable
+ private fun calculateSizeBucket(): SizeBucket {
+ val size: DpSize = LocalSize.current
+ val width = size.width
+
+ return when {
+ width < Small.width -> SizeBucket.Invalid
+ width <= Normal.width -> SizeBucket.Narrow
+ else -> SizeBucket.Normal
+ }
+ }
+
+ companion object {
+ const val TAG = "PodcasterAppWidget"
+ val Small = DpSize(128.dp, 48.dp)
+ val Normal = DpSize(256.dp, 124.dp)
+ }
+}
diff --git a/widget/src/main/kotlin/com/mr3y/podcaster/widget/PodcasterAppWidgetReceiver.kt b/widget/src/main/kotlin/com/mr3y/podcaster/widget/PodcasterAppWidgetReceiver.kt
new file mode 100644
index 00000000..eef62d5f
--- /dev/null
+++ b/widget/src/main/kotlin/com/mr3y/podcaster/widget/PodcasterAppWidgetReceiver.kt
@@ -0,0 +1,21 @@
+package com.mr3y.podcaster.widget
+
+import androidx.glance.appwidget.GlanceAppWidget
+import androidx.glance.appwidget.GlanceAppWidgetReceiver
+import com.mr3y.podcaster.core.data.PodcastsRepository
+import com.mr3y.podcaster.core.logger.Logger
+import dagger.hilt.android.AndroidEntryPoint
+import javax.inject.Inject
+
+@AndroidEntryPoint
+class PodcasterAppWidgetReceiver : GlanceAppWidgetReceiver() {
+
+ @Inject
+ lateinit var podcastsRepository: PodcastsRepository
+
+ @Inject
+ lateinit var logger: Logger
+
+ override val glanceAppWidget: GlanceAppWidget
+ get() = PodcasterAppWidget(podcastsRepository, logger)
+}
diff --git a/widget/src/main/res/drawable/outline_pause_24.xml b/widget/src/main/res/drawable/outline_pause_24.xml
new file mode 100644
index 00000000..9b16bde4
--- /dev/null
+++ b/widget/src/main/res/drawable/outline_pause_24.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/widget/src/main/res/drawable/outline_play_arrow_24.xml b/widget/src/main/res/drawable/outline_play_arrow_24.xml
new file mode 100644
index 00000000..91ab3ac1
--- /dev/null
+++ b/widget/src/main/res/drawable/outline_play_arrow_24.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/widget/src/main/res/drawable/outline_skip_next_24.xml b/widget/src/main/res/drawable/outline_skip_next_24.xml
new file mode 100644
index 00000000..a5b6207c
--- /dev/null
+++ b/widget/src/main/res/drawable/outline_skip_next_24.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/widget/src/main/res/drawable/outline_skip_prev_24.xml b/widget/src/main/res/drawable/outline_skip_prev_24.xml
new file mode 100644
index 00000000..9e4f956b
--- /dev/null
+++ b/widget/src/main/res/drawable/outline_skip_prev_24.xml
@@ -0,0 +1,13 @@
+
+
+
diff --git a/widget/src/main/res/values/strings.xml b/widget/src/main/res/values/strings.xml
new file mode 100644
index 00000000..a57e9591
--- /dev/null
+++ b/widget/src/main/res/values/strings.xml
@@ -0,0 +1,5 @@
+
+ Play your podcasts\' episodes
+ No episode is currently playing..
+ Invalid size, Resize your widget
+
\ No newline at end of file
diff --git a/widget/src/main/res/xml/podcaster_widget_info.xml b/widget/src/main/res/xml/podcaster_widget_info.xml
new file mode 100644
index 00000000..75dc8516
--- /dev/null
+++ b/widget/src/main/res/xml/podcaster_widget_info.xml
@@ -0,0 +1,16 @@
+
+
\ No newline at end of file