You've already forked immich
mirror of
https://github.com/immich-app/immich.git
synced 2025-07-06 06:07:29 +02:00
add configurable widget
This commit is contained in:
@ -114,10 +114,14 @@ dependencies {
|
|||||||
|
|
||||||
//Glance Widget
|
//Glance Widget
|
||||||
implementation "androidx.glance:glance-appwidget:$compose_version"
|
implementation "androidx.glance:glance-appwidget:$compose_version"
|
||||||
implementation("com.google.code.gson:gson:$gson_version")
|
implementation "com.google.code.gson:gson:$gson_version"
|
||||||
|
|
||||||
implementation("io.coil-kt.coil3:coil-compose:$coil_version")
|
// Glance Configure
|
||||||
implementation("io.coil-kt.coil3:coil-network-okhttp:$coil_version")
|
implementation "androidx.activity:activity-compose:1.8.2"
|
||||||
|
implementation "androidx.compose.ui:ui:$compose_version"
|
||||||
|
implementation "androidx.compose.ui:ui-tooling:$compose_version"
|
||||||
|
implementation "androidx.compose.material3:material3:1.2.1"
|
||||||
|
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.2"
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is uncommented in F-Droid build script
|
// This is uncommented in F-Droid build script
|
||||||
|
@ -152,7 +152,7 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.appwidget.provider"
|
android:name="android.appwidget.provider"
|
||||||
android:resource="@xml/widget" />
|
android:resource="@xml/random_widget" />
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
<!-- <receiver-->
|
<!-- <receiver-->
|
||||||
@ -165,6 +165,16 @@
|
|||||||
<!-- android:name="android.appwidget.provider"-->
|
<!-- android:name="android.appwidget.provider"-->
|
||||||
<!-- android:resource="@xml/widget" />-->
|
<!-- android:resource="@xml/widget" />-->
|
||||||
<!-- </receiver>-->
|
<!-- </receiver>-->
|
||||||
|
|
||||||
|
<activity android:name=".widget.configure.RandomConfigure"
|
||||||
|
android:exported="true"
|
||||||
|
android:theme="@style/Theme.Material3.DayNight.NoActionBar">
|
||||||
|
>
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
|
|
||||||
|
@ -4,7 +4,6 @@ import android.content.Context
|
|||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.datastore.preferences.core.Preferences
|
import androidx.datastore.preferences.core.Preferences
|
||||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
|
||||||
import androidx.glance.*
|
import androidx.glance.*
|
||||||
import androidx.glance.appwidget.GlanceAppWidgetManager
|
import androidx.glance.appwidget.GlanceAppWidgetManager
|
||||||
import androidx.glance.appwidget.state.updateAppWidgetState
|
import androidx.glance.appwidget.state.updateAppWidgetState
|
||||||
@ -15,7 +14,6 @@ import java.io.File
|
|||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import androidx.datastore.preferences.core.longPreferencesKey
|
|
||||||
import androidx.glance.appwidget.state.getAppWidgetState
|
import androidx.glance.appwidget.state.getAppWidgetState
|
||||||
import androidx.glance.state.PreferencesGlanceStateDefinition
|
import androidx.glance.state.PreferencesGlanceStateDefinition
|
||||||
|
|
||||||
@ -28,23 +26,27 @@ class ImageDownloadWorker(
|
|||||||
|
|
||||||
private val uniqueWorkName = ImageDownloadWorker::class.java.simpleName
|
private val uniqueWorkName = ImageDownloadWorker::class.java.simpleName
|
||||||
|
|
||||||
fun enqueue(context: Context, appWidgetId: Int, widgetType: WidgetType) {
|
private fun buildConstraints(): Constraints {
|
||||||
|
return Constraints.Builder()
|
||||||
|
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildInputData(appWidgetId: Int, widgetType: WidgetType): Data {
|
||||||
|
return Data.Builder()
|
||||||
|
.putString(kWorkerWidgetType, widgetType.toString())
|
||||||
|
.putInt(kWorkerWidgetID, appWidgetId)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun enqueuePeriodic(context: Context, appWidgetId: Int, widgetType: WidgetType) {
|
||||||
val manager = WorkManager.getInstance(context)
|
val manager = WorkManager.getInstance(context)
|
||||||
|
|
||||||
val workRequest = PeriodicWorkRequestBuilder<ImageDownloadWorker>(
|
val workRequest = PeriodicWorkRequestBuilder<ImageDownloadWorker>(
|
||||||
20, TimeUnit.MINUTES
|
20, TimeUnit.MINUTES
|
||||||
)
|
)
|
||||||
.setConstraints(
|
.setConstraints(buildConstraints())
|
||||||
Constraints.Builder()
|
.setInputData(buildInputData(appWidgetId, widgetType))
|
||||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.setInputData(
|
|
||||||
Data.Builder()
|
|
||||||
.putString("widgetType", widgetType.toString())
|
|
||||||
.putInt("widgetId", appWidgetId)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.addTag(appWidgetId.toString())
|
.addTag(appWidgetId.toString())
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
@ -55,21 +57,35 @@ class ImageDownloadWorker(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun singleShot(context: Context, appWidgetId: Int, widgetType: WidgetType) {
|
||||||
|
val manager = WorkManager.getInstance(context)
|
||||||
|
|
||||||
|
val workRequest = OneTimeWorkRequestBuilder<ImageDownloadWorker>()
|
||||||
|
.setConstraints(buildConstraints())
|
||||||
|
.setInputData(buildInputData(appWidgetId, widgetType))
|
||||||
|
.addTag(appWidgetId.toString())
|
||||||
|
.build()
|
||||||
|
|
||||||
|
manager.enqueueUniqueWork(
|
||||||
|
"$uniqueWorkName-$appWidgetId",
|
||||||
|
ExistingWorkPolicy.REPLACE,
|
||||||
|
workRequest
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun cancel(context: Context, glanceId: GlanceId) {
|
fun cancel(context: Context, glanceId: GlanceId) {
|
||||||
val appWidgetId = GlanceAppWidgetManager(context).getAppWidgetId(glanceId)
|
val appWidgetId = GlanceAppWidgetManager(context).getAppWidgetId(glanceId)
|
||||||
WorkManager.getInstance(context).cancelAllWorkByTag(appWidgetId.toString())
|
WorkManager.getInstance(context).cancelAllWorkByTag(appWidgetId.toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
return try {
|
return try {
|
||||||
val widgetType = WidgetType.valueOf(inputData.getString("config") ?: "")
|
val widgetType = WidgetType.valueOf(inputData.getString(kWorkerWidgetType) ?: "")
|
||||||
val widgetId = inputData.getInt("widgetId", -1)
|
val widgetId = inputData.getInt(kWorkerWidgetID, -1)
|
||||||
val glanceId = GlanceAppWidgetManager(context).getGlanceIdBy(widgetId)
|
val glanceId = GlanceAppWidgetManager(context).getGlanceIdBy(widgetId)
|
||||||
val currentState = getAppWidgetState(context, PreferencesGlanceStateDefinition, glanceId)
|
val currentState = getAppWidgetState(context, PreferencesGlanceStateDefinition, glanceId)
|
||||||
val currentImgUUID = currentState[stringPreferencesKey("uuid")]
|
val currentImgUUID = currentState[kImageUUID]
|
||||||
|
|
||||||
val serverConfig = ImmichAPI.getServerConfig(context)
|
val serverConfig = ImmichAPI.getServerConfig(context)
|
||||||
|
|
||||||
@ -112,9 +128,9 @@ class ImageDownloadWorker(
|
|||||||
|
|
||||||
private suspend fun updateWidget(type: WidgetType, glanceId: GlanceId, imageUUID: String, widgetState: WidgetState = WidgetState.SUCCESS) {
|
private suspend fun updateWidget(type: WidgetType, glanceId: GlanceId, imageUUID: String, widgetState: WidgetState = WidgetState.SUCCESS) {
|
||||||
updateAppWidgetState(context, glanceId) { prefs ->
|
updateAppWidgetState(context, glanceId) { prefs ->
|
||||||
prefs[longPreferencesKey("now")] = System.currentTimeMillis()
|
prefs[kNow] = System.currentTimeMillis()
|
||||||
prefs[stringPreferencesKey("uuid")] = imageUUID
|
prefs[kImageUUID] = imageUUID
|
||||||
prefs[stringPreferencesKey("state")] = widgetState.toString()
|
prefs[kWidgetState] = widgetState.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
when (type) {
|
when (type) {
|
||||||
@ -126,7 +142,7 @@ class ImageDownloadWorker(
|
|||||||
val api = ImmichAPI(serverConfig)
|
val api = ImmichAPI(serverConfig)
|
||||||
|
|
||||||
val filters = SearchFilters(AssetType.IMAGE, size=1)
|
val filters = SearchFilters(AssetType.IMAGE, size=1)
|
||||||
val albumId = widgetData[stringPreferencesKey("albumID")]
|
val albumId = widgetData[kSelectedAlbum]
|
||||||
|
|
||||||
if (albumId != null) {
|
if (albumId != null) {
|
||||||
filters.albumIds = listOf(albumId)
|
filters.albumIds = listOf(albumId)
|
||||||
@ -139,13 +155,13 @@ class ImageDownloadWorker(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun deleteImage(uuid: String) = withContext(Dispatchers.IO) {
|
private suspend fun deleteImage(uuid: String) = withContext(Dispatchers.IO) {
|
||||||
val file = File(context.cacheDir, "widget_image_$uuid.jpg")
|
val file = File(context.cacheDir, imageFilename(uuid))
|
||||||
file.delete()
|
file.delete()
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun saveImage(bitmap: Bitmap): String = withContext(Dispatchers.IO) {
|
private suspend fun saveImage(bitmap: Bitmap): String = withContext(Dispatchers.IO) {
|
||||||
val uuid = UUID.randomUUID().toString()
|
val uuid = UUID.randomUUID().toString()
|
||||||
val file = File(context.cacheDir, "widget_image_$uuid.jpg")
|
val file = File(context.cacheDir, imageFilename(uuid))
|
||||||
FileOutputStream(file).use { out ->
|
FileOutputStream(file).use { out ->
|
||||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, out)
|
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, out)
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
package app.alextran.immich.widget
|
package app.alextran.immich.widget
|
||||||
|
|
||||||
|
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||||
|
import androidx.datastore.preferences.core.doublePreferencesKey
|
||||||
|
import androidx.datastore.preferences.core.longPreferencesKey
|
||||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
import androidx.glance.appwidget.GlanceAppWidget
|
import androidx.glance.appwidget.GlanceAppWidget
|
||||||
|
|
||||||
@ -51,3 +54,17 @@ enum class WidgetState {
|
|||||||
|
|
||||||
data class ServerConfig(val serverEndpoint: String, val sessionKey: String)
|
data class ServerConfig(val serverEndpoint: String, val sessionKey: String)
|
||||||
|
|
||||||
|
// MARK: Widget State Keys
|
||||||
|
val kImageUUID = stringPreferencesKey("uuid")
|
||||||
|
val kSubtitleText = stringPreferencesKey("subtitle")
|
||||||
|
val kNow = longPreferencesKey("now")
|
||||||
|
val kWidgetState = stringPreferencesKey("state")
|
||||||
|
val kSelectedAlbum = stringPreferencesKey("albumID")
|
||||||
|
val kShowAlbumName = booleanPreferencesKey("showAlbumName")
|
||||||
|
|
||||||
|
const val kWorkerWidgetType = "widgetType"
|
||||||
|
const val kWorkerWidgetID = "widgetId"
|
||||||
|
|
||||||
|
fun imageFilename(id: String): String {
|
||||||
|
return "widget_image_$id.jpg"
|
||||||
|
}
|
||||||
|
@ -16,7 +16,7 @@ class RandomReceiver : HomeWidgetGlanceWidgetReceiver<RandomWidget>() {
|
|||||||
super.onUpdate(context, appWidgetManager, appWidgetIds)
|
super.onUpdate(context, appWidgetManager, appWidgetIds)
|
||||||
|
|
||||||
appWidgetIds.forEach { widgetID ->
|
appWidgetIds.forEach { widgetID ->
|
||||||
ImageDownloadWorker.enqueue(context, widgetID, WidgetType.RANDOM)
|
ImageDownloadWorker.enqueuePeriodic(context, widgetID, WidgetType.RANDOM)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,15 +17,15 @@ class RandomWidget : GlanceAppWidget() {
|
|||||||
|
|
||||||
provideContent {
|
provideContent {
|
||||||
val prefs = currentState<MutablePreferences>()
|
val prefs = currentState<MutablePreferences>()
|
||||||
val imageUUID = prefs[stringPreferencesKey("uuid")]
|
val imageUUID = prefs[kImageUUID]
|
||||||
|
|
||||||
val subtitle: String? = prefs[stringPreferencesKey("subtitle")]
|
val subtitle: String? = prefs[kSubtitleText]
|
||||||
var bitmap: Bitmap? = null
|
var bitmap: Bitmap? = null
|
||||||
var loggedIn = true
|
var loggedIn = true
|
||||||
|
|
||||||
if (imageUUID != null) {
|
if (imageUUID != null) {
|
||||||
// fetch a random photo from server
|
// fetch a random photo from server
|
||||||
val file = File(context.cacheDir, "widget_image_$imageUUID.jpg")
|
val file = File(context.cacheDir, imageFilename(id))
|
||||||
|
|
||||||
if (file.exists()) {
|
if (file.exists()) {
|
||||||
bitmap = loadScaledBitmap(file, 500, 500)
|
bitmap = loadScaledBitmap(file, 500, 500)
|
||||||
|
@ -0,0 +1,72 @@
|
|||||||
|
package app.alextran.immich.widget.configure
|
||||||
|
|
||||||
|
import androidx.compose.foundation.*
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.*
|
||||||
|
import androidx.compose.ui.geometry.*
|
||||||
|
import androidx.compose.ui.layout.*
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.unit.*
|
||||||
|
|
||||||
|
data class DropdownItem (
|
||||||
|
val label: String,
|
||||||
|
val id: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Creating a composable to display a drop down menu
|
||||||
|
@Composable
|
||||||
|
fun Dropdown(items: List<DropdownItem>,
|
||||||
|
selectedItem: DropdownItem?,
|
||||||
|
onItemSelected: (DropdownItem) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
label: String = "",) {
|
||||||
|
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
var textFieldSize by remember { mutableStateOf(Size.Zero) }
|
||||||
|
|
||||||
|
// Toggle icon based on expanded state
|
||||||
|
val icon = if (expanded)
|
||||||
|
Icons.Filled.KeyboardArrowUp
|
||||||
|
else
|
||||||
|
Icons.Filled.KeyboardArrowDown
|
||||||
|
|
||||||
|
Column(modifier) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = selectedItem?.label ?: "",
|
||||||
|
onValueChange = {},
|
||||||
|
readOnly = true,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.onGloballyPositioned { coordinates ->
|
||||||
|
textFieldSize = coordinates.size.toSize()
|
||||||
|
},
|
||||||
|
trailingIcon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = "Dropdown icon",
|
||||||
|
modifier = Modifier.clickable { expanded = !expanded }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = expanded,
|
||||||
|
onDismissRequest = { expanded = false },
|
||||||
|
modifier = Modifier.width(with(LocalDensity.current) { textFieldSize.width.toDp() })
|
||||||
|
) {
|
||||||
|
items.forEach { item ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(text = item.label) },
|
||||||
|
onClick = {
|
||||||
|
onItemSelected(item)
|
||||||
|
expanded = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
package app.alextran.immich.widget.configure
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.compose.foundation.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.material3.Typography
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LightDarkTheme(
|
||||||
|
useDarkTheme: Boolean = isSystemInDarkTheme(), // ← This line is key
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
|
|
||||||
|
val colorScheme = when {
|
||||||
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && isDarkTheme ->
|
||||||
|
dynamicDarkColorScheme(context)
|
||||||
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !isDarkTheme ->
|
||||||
|
dynamicLightColorScheme(context)
|
||||||
|
isDarkTheme -> darkColorScheme()
|
||||||
|
else -> lightColorScheme()
|
||||||
|
}
|
||||||
|
MaterialTheme(
|
||||||
|
colorScheme = colorScheme,
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,173 @@
|
|||||||
|
package app.alextran.immich.widget.configure
|
||||||
|
|
||||||
|
import android.appwidget.AppWidgetManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.glance.GlanceId
|
||||||
|
import androidx.glance.appwidget.GlanceAppWidgetManager
|
||||||
|
import androidx.glance.appwidget.state.getAppWidgetState
|
||||||
|
import androidx.glance.appwidget.state.updateAppWidgetState
|
||||||
|
import androidx.glance.state.PreferencesGlanceStateDefinition
|
||||||
|
import app.alextran.immich.widget.ImageDownloadWorker
|
||||||
|
import app.alextran.immich.widget.ImmichAPI
|
||||||
|
import app.alextran.immich.widget.WidgetState
|
||||||
|
import app.alextran.immich.widget.WidgetType
|
||||||
|
import app.alextran.immich.widget.kSelectedAlbum
|
||||||
|
import app.alextran.immich.widget.kShowAlbumName
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class RandomConfigure : ComponentActivity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
// Get widget ID from intent
|
||||||
|
val appWidgetId = intent?.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
|
||||||
|
AppWidgetManager.INVALID_APPWIDGET_ID)
|
||||||
|
?: AppWidgetManager.INVALID_APPWIDGET_ID
|
||||||
|
|
||||||
|
if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
|
||||||
|
finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val glanceId = GlanceAppWidgetManager(applicationContext)
|
||||||
|
.getGlanceIdBy(appWidgetId)
|
||||||
|
|
||||||
|
setContent {
|
||||||
|
LightDarkTheme {
|
||||||
|
RandomConfiguration(applicationContext, appWidgetId, glanceId, onDone = {
|
||||||
|
finish()
|
||||||
|
Log.w("WIDGET_ACTIVITY", "SAVING")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun RandomConfiguration(context: Context, appWidgetId: Int, glanceId: GlanceId, onDone: () -> Unit) {
|
||||||
|
|
||||||
|
var selectedAlbum by remember { mutableStateOf<DropdownItem?>(null) }
|
||||||
|
var showAlbumName by remember { mutableStateOf(false) }
|
||||||
|
var availableAlbums by remember { mutableStateOf<List<DropdownItem>>(listOf()) }
|
||||||
|
var state by remember { mutableStateOf(WidgetState.LOADING) }
|
||||||
|
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
// get albums from server
|
||||||
|
val serverCfg = ImmichAPI.getServerConfig(context)
|
||||||
|
val api: ImmichAPI?
|
||||||
|
|
||||||
|
if (serverCfg == null) {
|
||||||
|
state = WidgetState.LOG_IN
|
||||||
|
return@LaunchedEffect
|
||||||
|
}
|
||||||
|
|
||||||
|
api = ImmichAPI(serverCfg)
|
||||||
|
val albumItems = api.fetchAlbums().map {
|
||||||
|
DropdownItem(it.albumName, it.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
availableAlbums = listOf(DropdownItem("None", "NONE")) + albumItems
|
||||||
|
state = WidgetState.SUCCESS
|
||||||
|
|
||||||
|
// load selected configuration
|
||||||
|
val currentState = getAppWidgetState(context, PreferencesGlanceStateDefinition, glanceId)
|
||||||
|
val currentAlbumId = currentState[kSelectedAlbum]
|
||||||
|
val albumEntity = availableAlbums.firstOrNull { it.id == currentAlbumId }
|
||||||
|
selectedAlbum = albumEntity ?: availableAlbums.first()
|
||||||
|
|
||||||
|
// load showAlbumName
|
||||||
|
showAlbumName = currentState[kShowAlbumName] ?: false
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun saveConfiguration() {
|
||||||
|
updateAppWidgetState(context, glanceId) { prefs ->
|
||||||
|
prefs[kSelectedAlbum] = selectedAlbum?.id ?: ""
|
||||||
|
prefs[kShowAlbumName] = showAlbumName
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageDownloadWorker.singleShot(context, appWidgetId, WidgetType.RANDOM)
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar (
|
||||||
|
title = { Text("Widget Configuration") },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = {
|
||||||
|
scope.launch {
|
||||||
|
saveConfiguration()
|
||||||
|
onDone()
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Icon(Icons.Default.Close, contentDescription = "Close")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { innerPadding ->
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(innerPadding), // Respect the top bar
|
||||||
|
color = MaterialTheme.colorScheme.background
|
||||||
|
) {
|
||||||
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.TopCenter) {
|
||||||
|
when (state) {
|
||||||
|
WidgetState.LOADING -> CircularProgressIndicator(modifier = Modifier.size(48.dp))
|
||||||
|
WidgetState.LOG_IN -> Text("You must log in inside the Immich App to configure this widget.")
|
||||||
|
WidgetState.SUCCESS -> {
|
||||||
|
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
Text("View a random image from your library or a specific album.", style = MaterialTheme.typography.bodyMedium)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceContainer)
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Text("Album")
|
||||||
|
Dropdown(
|
||||||
|
items = availableAlbums,
|
||||||
|
selectedItem = selectedAlbum,
|
||||||
|
onItemSelected = { selectedAlbum = it },
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text(text = "Show Album Name")
|
||||||
|
Switch(
|
||||||
|
checked = showAlbumName,
|
||||||
|
onCheckedChange = { showAlbumName = it }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
11
mobile/android/app/src/main/res/xml/random_widget.xml
Normal file
11
mobile/android/app/src/main/res/xml/random_widget.xml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:initialLayout="@layout/glance_default_loading_layout"
|
||||||
|
android:minWidth="110dp"
|
||||||
|
android:minHeight="110dp"
|
||||||
|
android:resizeMode="horizontal|vertical"
|
||||||
|
android:updatePeriodMillis="1200000"
|
||||||
|
android:configure="app.alextran.immich.widget.configure.RandomConfigure"
|
||||||
|
android:widgetFeatures="reconfigurable|configuration_optional"
|
||||||
|
tools:targetApi="28"
|
||||||
|
/>
|
@ -1,7 +0,0 @@
|
|||||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:initialLayout="@layout/glance_default_loading_layout"
|
|
||||||
android:minWidth="110dp"
|
|
||||||
android:minHeight="110dp"
|
|
||||||
android:resizeMode="horizontal|vertical"
|
|
||||||
android:updatePeriodMillis="1200000"
|
|
||||||
/>
|
|
Reference in New Issue
Block a user