widgets: reload data on update, clock widget, picker previews; README screenshots; 0.2.0
- Glance update() only recomposed the running session, so saved styles (opacity, colours) and new forecasts never reached placed widgets; a state version now triggers a reload - new minimal clock widget (live TextClock, date, current weather), transparent by default - static preview images for the widget picker, generated previews on Android 15+ - debug builds install next to the release app (app.wetter.debug) Co-Authored-By: Claude Opus 5.5 <[email protected]>
@@ -20,9 +20,26 @@ A weather app with the look and feel of Pixel Weather: an animated sky that chan
|
||||
- **No account, no tracking, no analytics.** Everything stays on the device. Coordinates are rounded to two decimals (about 1 km) before they reach the weather service.
|
||||
- **Open data sources only:** [Open-Meteo](https://open-meteo.com) for forecasts, 15-minute precipitation, air quality, pollen and place search (CC BY 4.0), and [Nominatim / OpenStreetMap](https://nominatim.org) for naming your current location.
|
||||
- **Features today:** current location and saved cities (swipe between them), search with a full preview before saving, precipitation-in-the-next-hours card, a rule-based daily summary, day detail view, units and theme settings, light and dark mode.
|
||||
- **Five home-screen widgets:** a weather card that grows from 3×2 to 4×5 (hourly panel, then a daily list), a pill, a tilted pill, a one-line bar with a cookie badge, and a classic card. Each one can be customised when added or later: location, system/light/dark, colours (wallpaper Material You, current weather, or five fixed palettes, all WCAG AA checked) and background opacity from 100 % down to fully transparent.
|
||||
- **Six home-screen widgets:** a minimal clock with date and current weather (transparent by default), a weather card that grows from 3×2 to 4×5 (hourly panel, then a daily list), a pill, a tilted pill, a one-line bar with a cookie badge, and a classic card. The widget picker shows real previews. Each one can be customised when added or later: location, system/light/dark, colours (wallpaper Material You, current weather, or five fixed palettes, all WCAG AA checked) and background opacity from 100 % down to fully transparent.
|
||||
- **Coming next:** severe weather alerts (DWD / MeteoAlarm), precipitation radar map, card reordering, notifications.
|
||||
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/screenshots/S01-clear-day.png" width="200" alt="Home, sunny">
|
||||
<img src="docs/screenshots/S01-clear-night-dark.png" width="200" alt="Home, clear night, dark mode">
|
||||
<img src="docs/screenshots/S01-rain-nowcast.png" width="200" alt="Rain with the next-hours precipitation card">
|
||||
</p>
|
||||
<p align="center">
|
||||
<img src="docs/screenshots/S01-full-light.png" width="200" alt="Full forecast with detail tiles">
|
||||
<img src="docs/screenshots/S04-day-dark.png" width="200" alt="Day detail">
|
||||
<img src="docs/screenshots/S08-settings.png" width="200" alt="Settings">
|
||||
</p>
|
||||
<p align="center">
|
||||
<img src="docs/screenshots/widgets.png" width="380" alt="Home-screen widgets: clock, weather card, pill, bar, classic, tilted pill">
|
||||
</p>
|
||||
|
||||
<sub>Screenshots are rendered from a real Open-Meteo response (Berlin) by the app's screenshot tests; widgets are rendered on a Pixel with the wallpaper colours of that phone.</sub>
|
||||
|
||||
Requires Android 10 (API 29) or newer.
|
||||
|
||||
## Install with Obtainium
|
||||
|
||||
|
After Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 127 KiB |
|
After Width: | Height: | Size: 261 KiB |
|
After Width: | Height: | Size: 157 KiB |
|
After Width: | Height: | Size: 127 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 87 KiB |
@@ -17,8 +17,8 @@ android {
|
||||
applicationId = "app.wetter"
|
||||
minSdk = 29
|
||||
targetSdk = 36
|
||||
versionCode = 1
|
||||
versionName = "0.1.0"
|
||||
versionCode = 2
|
||||
versionName = "0.2.0"
|
||||
}
|
||||
|
||||
// Release key lives outside the repo; without it, release builds fall back to the debug key (local only).
|
||||
@@ -36,7 +36,13 @@ android {
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
// installs next to the release app from Obtainium
|
||||
applicationIdSuffix = ".debug"
|
||||
resValue("string", "app_name", "Wetter (Debug)")
|
||||
}
|
||||
release {
|
||||
resValue("string", "app_name", "Wetter")
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
signingConfig = signingConfigs.findByName("release") ?: signingConfigs.getByName("debug")
|
||||
|
||||
@@ -50,6 +50,7 @@ class WidgetGalleryActivity : androidx.activity.ComponentActivity() {
|
||||
PillWidget() to DpSize(170.dp, 64.dp),
|
||||
),
|
||||
)
|
||||
if (intent.getBooleanExtra("export", false)) { exportPreviews(column); return }
|
||||
lifecycleScope.launch {
|
||||
for ((widget, size) in pages[page.coerceIn(0, pages.lastIndex)]) {
|
||||
val views = widget.compose(this@WidgetGalleryActivity, size = size)
|
||||
@@ -60,6 +61,48 @@ class WidgetGalleryActivity : androidx.activity.ComponentActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
/** Renders each widget at its default size and writes preview_<key>.png to external files. */
|
||||
private fun exportPreviews(column: LinearLayout) {
|
||||
val items = listOf(
|
||||
"card" to (WeatherCardWidget() to DpSize(300.dp, 230.dp)),
|
||||
"pill" to (PillWidget() to DpSize(280.dp, 120.dp)),
|
||||
"tilt" to (TiltWidget() to DpSize(160.dp, 160.dp)),
|
||||
"bar" to (BarWidget() to DpSize(300.dp, 80.dp)),
|
||||
"classic" to (ClassicWidget() to DpSize(300.dp, 170.dp)),
|
||||
"clock" to (app.wetter.widget.ClockWidget() to DpSize(300.dp, 140.dp)),
|
||||
)
|
||||
lifecycleScope.launch {
|
||||
val frames = mutableListOf<Pair<String, FrameLayout>>()
|
||||
for ((key, pair) in items) {
|
||||
val (widget, size) = pair
|
||||
val frame = FrameLayout(this@WidgetGalleryActivity)
|
||||
if (key == "clock") {
|
||||
// the clock is transparent: show it over a soft wallpaper-like backdrop
|
||||
app.wetter.widget.WidgetPreviewOverride.config =
|
||||
app.wetter.data.WidgetConfigEntity(-1, null, "wallpaper", "dark", 0)
|
||||
frame.background = android.graphics.drawable.GradientDrawable(
|
||||
android.graphics.drawable.GradientDrawable.Orientation.TL_BR,
|
||||
intArrayOf(Color.rgb(74, 98, 120), Color.rgb(150, 128, 112)),
|
||||
).apply { cornerRadius = px(28).toFloat() }
|
||||
}
|
||||
val views = widget.compose(this@WidgetGalleryActivity, size = size)
|
||||
app.wetter.widget.WidgetPreviewOverride.config = null
|
||||
frame.addView(views.apply(this@WidgetGalleryActivity, frame))
|
||||
column.addView(frame, LinearLayout.LayoutParams(px(size.width.value), px(size.height.value)).apply { bottomMargin = px(12) })
|
||||
frames += key to frame
|
||||
}
|
||||
column.postDelayed({
|
||||
val dir = getExternalFilesDir(null)!!
|
||||
for ((key, frame) in frames) {
|
||||
val bmp = android.graphics.Bitmap.createBitmap(frame.width, frame.height, android.graphics.Bitmap.Config.ARGB_8888)
|
||||
frame.draw(android.graphics.Canvas(bmp))
|
||||
java.io.File(dir, "preview_$key.png").outputStream().use { bmp.compress(android.graphics.Bitmap.CompressFormat.PNG, 100, it) }
|
||||
}
|
||||
android.util.Log.i("WidgetGallery", "exported ${frames.size}")
|
||||
}, 1500)
|
||||
}
|
||||
}
|
||||
|
||||
private fun px(dp: Float) = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, resources.displayMetrics).toInt()
|
||||
private fun px(dp: Int) = px(dp.toFloat())
|
||||
}
|
||||
|
||||
@@ -29,6 +29,10 @@
|
||||
<intent-filter><action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" /></intent-filter>
|
||||
</activity>
|
||||
|
||||
<receiver android:name=".widget.ClockReceiver" android:exported="true" android:label="@string/widget_clock_name">
|
||||
<intent-filter><action android:name="android.appwidget.action.APPWIDGET_UPDATE" /></intent-filter>
|
||||
<meta-data android:name="android.appwidget.provider" android:resource="@xml/widget_clock" />
|
||||
</receiver>
|
||||
<receiver android:name=".widget.WeatherCardReceiver" android:exported="true" android:label="@string/widget_card_name">
|
||||
<intent-filter><action android:name="android.appwidget.action.APPWIDGET_UPDATE" /></intent-filter>
|
||||
<meta-data android:name="android.appwidget.provider" android:resource="@xml/widget_card" />
|
||||
|
||||
@@ -100,7 +100,7 @@ class WidgetConfigActivity : ComponentActivity() {
|
||||
ConfigScreen(appWidgetId, widget, size) { config ->
|
||||
lifecycleScope.launch {
|
||||
Graph.repo.saveWidgetConfig(config)
|
||||
runCatching { widget.update(this@WidgetConfigActivity, GlanceAppWidgetManager(this@WidgetConfigActivity).getGlanceIdBy(appWidgetId)) }
|
||||
runCatching { refreshWidget(this@WidgetConfigActivity, widget, GlanceAppWidgetManager(this@WidgetConfigActivity).getGlanceIdBy(appWidgetId)) }
|
||||
setResult(RESULT_OK, Intent().putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId))
|
||||
finish()
|
||||
}
|
||||
@@ -114,6 +114,7 @@ class WidgetConfigActivity : ComponentActivity() {
|
||||
providerClass.endsWith("PillReceiver") -> PillWidget() to DpSize(300.dp, 130.dp)
|
||||
providerClass.endsWith("TiltReceiver") -> TiltWidget() to DpSize(170.dp, 170.dp)
|
||||
providerClass.endsWith("BarReceiver") -> BarWidget() to DpSize(320.dp, 84.dp)
|
||||
providerClass.endsWith("ClockReceiver") -> ClockWidget() to DpSize(320.dp, 150.dp)
|
||||
providerClass.endsWith("ClassicReceiver") -> ClassicWidget() to DpSize(320.dp, 160.dp)
|
||||
else -> WeatherCardWidget() to DpSize(320.dp, 240.dp)
|
||||
}
|
||||
@@ -125,7 +126,10 @@ class WidgetConfigActivity : ComponentActivity() {
|
||||
private fun ConfigScreen(appWidgetId: Int, widget: GlanceAppWidget, size: DpSize, onDone: (WidgetConfigEntity) -> Unit) {
|
||||
val context = LocalContext.current
|
||||
val locations by Graph.repo.locations.collectAsState(initial = emptyList())
|
||||
var config by remember { mutableStateOf(WidgetConfigEntity(appWidgetId, null)) }
|
||||
var config by remember {
|
||||
val d = (widget as? WeatherWidget)?.defaultStyle ?: WidgetStyle()
|
||||
mutableStateOf(WidgetConfigEntity(appWidgetId, null, d.palette, d.theme, d.opacity))
|
||||
}
|
||||
var loaded by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(appWidgetId) {
|
||||
Graph.repo.widgetConfig(appWidgetId)?.let { config = it }
|
||||
|
||||
@@ -54,22 +54,46 @@ import app.wetter.data.conditionOf
|
||||
import app.wetter.ui.components.conditionLabel
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlin.reflect.KClass
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.datastore.preferences.core.longPreferencesKey
|
||||
import androidx.glance.appwidget.state.updateAppWidgetState
|
||||
import androidx.glance.currentState
|
||||
|
||||
private val DATA_VERSION = longPreferencesKey("data_version")
|
||||
|
||||
/** All widget types, paired with their receivers. */
|
||||
val ALL_WIDGETS: List<Pair<() -> WeatherWidget, KClass<out GlanceAppWidgetReceiver>>> = listOf(
|
||||
::WeatherCardWidget to WeatherCardReceiver::class,
|
||||
::PillWidget to PillReceiver::class,
|
||||
::TiltWidget to TiltReceiver::class,
|
||||
::BarWidget to BarReceiver::class,
|
||||
::ClassicWidget to ClassicReceiver::class,
|
||||
::ClockWidget to ClockReceiver::class,
|
||||
)
|
||||
|
||||
/**
|
||||
* Glance's update() only recomposes a running session, it does not run provideGlance again.
|
||||
* Bumping a value in the widget's state makes the composition reload settings and forecast.
|
||||
*/
|
||||
suspend fun refreshWidget(context: Context, widget: GlanceAppWidget, id: GlanceId) {
|
||||
updateAppWidgetState(context, id) { it[DATA_VERSION] = System.nanoTime() }
|
||||
widget.update(context, id)
|
||||
}
|
||||
|
||||
suspend fun updateAllWidgets(context: Context) {
|
||||
WeatherCardWidget().updateAll(context)
|
||||
PillWidget().updateAll(context)
|
||||
TiltWidget().updateAll(context)
|
||||
BarWidget().updateAll(context)
|
||||
ClassicWidget().updateAll(context)
|
||||
val manager = GlanceAppWidgetManager(context)
|
||||
for ((make, receiver) in ALL_WIDGETS) {
|
||||
val widget = make()
|
||||
manager.getGlanceIds(widget.javaClass).forEach { runCatching { refreshWidget(context, widget, it) } }
|
||||
}
|
||||
}
|
||||
|
||||
/** Picker previews (Android 15+). The system rate-limits this, so failures are ignored. */
|
||||
suspend fun publishWidgetPreviews(context: Context) {
|
||||
if (Build.VERSION.SDK_INT < 35) return
|
||||
val manager = GlanceAppWidgetManager(context)
|
||||
listOf<KClass<out GlanceAppWidgetReceiver>>(
|
||||
WeatherCardReceiver::class, PillReceiver::class, TiltReceiver::class, BarReceiver::class, ClassicReceiver::class,
|
||||
).forEach { runCatching { manager.setWidgetPreviews(it) } }
|
||||
ALL_WIDGETS.forEach { (_, receiver) -> runCatching { manager.setWidgetPreviews(receiver) } }
|
||||
}
|
||||
|
||||
class WidgetData(val location: LocationEntity?, val forecast: Forecast?, val units: Units, val palette: WidgetPalette) {
|
||||
@@ -81,7 +105,7 @@ object WidgetPreviewOverride {
|
||||
@Volatile var config: app.wetter.data.WidgetConfigEntity? = null
|
||||
}
|
||||
|
||||
suspend fun loadWidgetData(context: Context, id: GlanceId?): WidgetData {
|
||||
suspend fun loadWidgetData(context: Context, id: GlanceId?, defaultStyle: WidgetStyle = WidgetStyle()): WidgetData {
|
||||
Graph.init(context)
|
||||
// ids that are not real widgets (previews, the debug gallery) fall back to the current location
|
||||
val appWidgetId = id?.let { runCatching { GlanceAppWidgetManager(context).getAppWidgetId(it) }.getOrNull() } ?: -1
|
||||
@@ -89,19 +113,28 @@ suspend fun loadWidgetData(context: Context, id: GlanceId?): WidgetData {
|
||||
val location = config?.locationId?.let { Graph.repo.location(it) } ?: Graph.repo.widgetLocation(appWidgetId)
|
||||
val forecast = location?.let { Graph.repo.forecastOnce(it.id) }
|
||||
val settings = runCatching { Graph.settings.settings.first() }.getOrDefault(Settings())
|
||||
val style = config?.let { WidgetStyle(it.style, it.theme, it.opacity) } ?: WidgetStyle()
|
||||
val style = config?.let { WidgetStyle(it.style, it.theme, it.opacity) } ?: defaultStyle
|
||||
return WidgetData(location, forecast, Units(settings), resolvePalette(context, style, forecast))
|
||||
}
|
||||
|
||||
/** Base: loads data once per update, renders [Content], and offers the same content as picker preview. */
|
||||
abstract class WeatherWidget : GlanceAppWidget() {
|
||||
/** Style used until the person configures this widget. */
|
||||
open val defaultStyle: WidgetStyle get() = WidgetStyle()
|
||||
|
||||
override suspend fun provideGlance(context: Context, id: GlanceId) {
|
||||
val data = loadWidgetData(context, id)
|
||||
provideContent { Root(data) }
|
||||
val first = loadWidgetData(context, id, defaultStyle)
|
||||
provideContent {
|
||||
val version = currentState(DATA_VERSION) ?: 0L
|
||||
val data by produceState(first, version) { if (version != 0L) value = loadWidgetData(context, id, defaultStyle) }
|
||||
Root(data)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun providePreview(context: Context, widgetCategory: Int) {
|
||||
val data = loadWidgetData(context, null)
|
||||
// a transparent widget would vanish against the picker, so previews always get a background
|
||||
val style = defaultStyle.let { if (it.opacity < 100) it.copy(opacity = 100) else it }
|
||||
val data = loadWidgetData(context, null, style)
|
||||
provideContent { Root(data) }
|
||||
}
|
||||
|
||||
@@ -404,4 +437,37 @@ class WeatherCardReceiver : GlanceAppWidgetReceiver() { override val glanceAppWi
|
||||
class PillReceiver : GlanceAppWidgetReceiver() { override val glanceAppWidget: GlanceAppWidget = PillWidget() }
|
||||
class TiltReceiver : GlanceAppWidgetReceiver() { override val glanceAppWidget: GlanceAppWidget = TiltWidget() }
|
||||
class BarReceiver : GlanceAppWidgetReceiver() { override val glanceAppWidget: GlanceAppWidget = BarWidget() }
|
||||
class ClockReceiver : GlanceAppWidgetReceiver() { override val glanceAppWidget: GlanceAppWidget = ClockWidget() }
|
||||
class ClassicReceiver : GlanceAppWidgetReceiver() { override val glanceAppWidget: GlanceAppWidget = ClassicWidget() }
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// 6. Clock: big live time, date and the current weather. Transparent by default, for a clean home screen
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
class ClockWidget : WeatherWidget() {
|
||||
override val sizeMode = SizeMode.Exact
|
||||
override val defaultStyle = WidgetStyle(palette = WidgetStyle.WALLPAPER, theme = WidgetStyle.DARK, opacity = 0)
|
||||
|
||||
@Composable
|
||||
override fun Content(data: WidgetData, f: Forecast) {
|
||||
val ctx = LocalContext.current
|
||||
val p = data.palette
|
||||
val u = data.units
|
||||
val size = LocalSize.current
|
||||
// on a bare wallpaper white text with a soft shadow reads best; with a background use the palette
|
||||
val fg = if (p.bg.alpha < .3f) Color.White else p.onBg
|
||||
val timeSp = (size.height.value * .42f).coerceIn(40f, 96f)
|
||||
val views = android.widget.RemoteViews(ctx.packageName, R.layout.widget_clock).apply {
|
||||
val c = android.graphics.Color.argb((fg.alpha * 255).toInt(), (fg.red * 255).toInt(), (fg.green * 255).toInt(), (fg.blue * 255).toInt())
|
||||
listOf(R.id.clock_time, R.id.clock_date, R.id.clock_sep, R.id.clock_temp).forEach { setTextColor(it, c) }
|
||||
setTextViewTextSize(R.id.clock_time, android.util.TypedValue.COMPLEX_UNIT_SP, timeSp)
|
||||
setImageViewBitmap(R.id.clock_icon, WidgetArt.icon(conditionOf(f.current.code), f.current.isDay, 96))
|
||||
setTextViewText(R.id.clock_temp, u.temp(f.current.temp))
|
||||
setContentDescription(R.id.clock_temp, "${u.temp(f.current.temp)} ${conditionText(f)}")
|
||||
}
|
||||
Box(
|
||||
GlanceModifier.fillMaxSize().cornerRadius(28.dp).background(p.bg).clickable(open(data)),
|
||||
contentAlignment = Alignment.Center,
|
||||
) { androidx.glance.appwidget.AndroidRemoteViews(views) }
|
||||
}
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent" android:layout_height="match_parent"
|
||||
android:orientation="vertical" android:gravity="center" android:padding="12dp">
|
||||
|
||||
<TextClock
|
||||
android:id="@+id/clock_time"
|
||||
android:layout_width="wrap_content" android:layout_height="wrap_content"
|
||||
android:format24Hour="HH:mm" android:format12Hour="h:mm"
|
||||
android:fontFamily="sans-serif-light" android:textSize="72sp" android:includeFontPadding="false"
|
||||
android:letterSpacing="-0.02"
|
||||
android:shadowColor="#66000000" android:shadowRadius="8" android:shadowDy="2" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content" android:layout_height="wrap_content"
|
||||
android:orientation="horizontal" android:gravity="center_vertical" android:layout_marginTop="4dp">
|
||||
|
||||
<TextClock
|
||||
android:id="@+id/clock_date"
|
||||
android:layout_width="wrap_content" android:layout_height="wrap_content"
|
||||
android:format24Hour="EEE, d. MMM" android:format12Hour="EEE, MMM d"
|
||||
android:fontFamily="sans-serif-medium" android:textSize="16sp"
|
||||
android:shadowColor="#66000000" android:shadowRadius="6" android:shadowDy="1" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/clock_sep"
|
||||
android:layout_width="wrap_content" android:layout_height="wrap_content"
|
||||
android:text=" · " android:textSize="16sp"
|
||||
android:shadowColor="#66000000" android:shadowRadius="6" android:shadowDy="1" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/clock_icon"
|
||||
android:layout_width="22dp" android:layout_height="22dp" android:importantForAccessibility="no" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/clock_temp"
|
||||
android:layout_width="wrap_content" android:layout_height="wrap_content"
|
||||
android:layout_marginStart="6dp"
|
||||
android:fontFamily="sans-serif-medium" android:textSize="16sp"
|
||||
android:shadowColor="#66000000" android:shadowRadius="6" android:shadowDy="1" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Wetter</string>
|
||||
|
||||
<string name="cond_clear_day">Sonnig</string>
|
||||
<string name="cond_clear_night">Klar</string>
|
||||
@@ -133,6 +132,8 @@
|
||||
<string-array name="widget_palettes">
|
||||
<item>Wallpaper</item><item>Wetterlage</item><item>Ozean</item><item>Wald</item><item>Sonne</item><item>Lavendel</item><item>Graphit</item>
|
||||
</string-array>
|
||||
<string name="widget_clock_name">Uhr & Wetter</string>
|
||||
<string name="widget_clock_desc">Große Uhrzeit, Datum und das aktuelle Wetter</string>
|
||||
<string name="widget_source">Quelle: Open-Meteo</string>
|
||||
<string name="widget_pick">Ort wählen</string>
|
||||
<string name="widget_follow">Mein Standort (folgt dir)</string>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:description="@string/widget_bar_desc"
|
||||
android:previewImage="@drawable/preview_bar"
|
||||
android:initialLayout="@layout/glance_default_loading_layout"
|
||||
android:minWidth="250dp" android:minHeight="60dp"
|
||||
android:targetCellWidth="4" android:targetCellHeight="1"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:description="@string/widget_card_desc"
|
||||
android:previewImage="@drawable/preview_card"
|
||||
android:initialLayout="@layout/glance_default_loading_layout"
|
||||
android:minWidth="250dp" android:minHeight="180dp"
|
||||
android:targetCellWidth="4" android:targetCellHeight="2"
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:description="@string/widget_classic_desc"
|
||||
android:previewImage="@drawable/preview_classic"
|
||||
android:initialLayout="@layout/glance_default_loading_layout"
|
||||
android:minWidth="250dp" android:minHeight="110dp"
|
||||
android:minWidth="250dp" android:minHeight="150dp"
|
||||
android:targetCellWidth="4" android:targetCellHeight="2"
|
||||
android:minResizeWidth="140dp" android:minResizeHeight="110dp"
|
||||
android:minResizeWidth="140dp" android:minResizeHeight="150dp"
|
||||
android:maxResizeWidth="400dp" android:maxResizeHeight="250dp"
|
||||
android:resizeMode="horizontal|vertical"
|
||||
android:updatePeriodMillis="3600000"
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:description="@string/widget_clock_desc"
|
||||
android:initialLayout="@layout/glance_default_loading_layout"
|
||||
android:previewImage="@drawable/preview_clock"
|
||||
android:minWidth="250dp" android:minHeight="110dp"
|
||||
android:targetCellWidth="4" android:targetCellHeight="2"
|
||||
android:minResizeWidth="180dp" android:minResizeHeight="80dp"
|
||||
android:maxResizeWidth="600dp" android:maxResizeHeight="300dp"
|
||||
android:resizeMode="horizontal|vertical"
|
||||
android:updatePeriodMillis="3600000"
|
||||
android:configure="app.wetter.widget.WidgetConfigActivity"
|
||||
android:widgetFeatures="reconfigurable|configuration_optional"
|
||||
android:widgetCategory="home_screen|keyguard" />
|
||||
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:description="@string/widget_pill_desc"
|
||||
android:previewImage="@drawable/preview_pill"
|
||||
android:initialLayout="@layout/glance_default_loading_layout"
|
||||
android:minWidth="200dp" android:minHeight="110dp"
|
||||
android:targetCellWidth="4" android:targetCellHeight="2"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:description="@string/widget_tilt_desc"
|
||||
android:previewImage="@drawable/preview_tilt"
|
||||
android:initialLayout="@layout/glance_default_loading_layout"
|
||||
android:minWidth="110dp" android:minHeight="110dp"
|
||||
android:targetCellWidth="2" android:targetCellHeight="2"
|
||||
|
||||