feat(v1.8.0): IMPL_019 Homescreen Widgets Implementation
Complete Jetpack Glance Widget Framework
- Implement NoteWidget with 5 responsive size classes
- Support TEXT and CHECKLIST note types
- Material You dynamic colors integration
- Interactive checklist checkboxes in large layouts
- Read-only mode for locked widgets
Widget Configuration System
- NoteWidgetConfigActivity for placement and reconfiguration (Android 12+)
- NoteWidgetConfigScreen with note selection and settings
- Lock widget toggle to prevent accidental edits
- Background opacity slider with 0-100% range
- Auto-save on back navigation plus Save FAB
Widget State Management
- NoteWidgetState keys for per-instance persistence via DataStore
- NoteWidgetActionKeys for type-safe parameter passing
- Five top-level ActionCallback classes
* ToggleChecklistItemAction (updates checklist and marks for sync)
* ToggleLockAction (toggle read-only mode)
* ShowOptionsAction (show permanent options bar)
* RefreshAction (reload from storage)
* OpenConfigAction (launch widget config activity)
Responsive Layout System
- SMALL (110x80dp): Title only
- NARROW_MEDIUM (110x110dp): Preview or compact checklist
- NARROW_TALL (110x250dp): Full content display
- WIDE_MEDIUM (250x110dp): Preview layout
- WIDE_TALL (250x250dp): Interactive checklist
Interactive Widget Features
- Tap content to open editor (unlock) or show options (lock)
- Checklist checkboxes with immediate state sync
- Options bar with Lock/Unlock, Refresh, Settings, Open in App buttons
- Per-widget background transparency control
Connection Leak Fixes (Part of IMPL_019)
- Override put/delete/createDirectory in SafeSardineWrapper with response.use{}
- Proper resource cleanup in exportAllNotesToMarkdown and syncMarkdownFiles
- Use modern OkHttp APIs (toMediaTypeOrNull, toRequestBody)
UI Improvements (Part of IMPL_019)
- Checkbox toggle includes KEY_LAST_UPDATED to force Glance recomposition
- Note selection in config is visual-only (separate from save)
- Config uses moveTaskToBack() plus FLAG_ACTIVITY_CLEAR_TASK
- Proper options bar with standard Material icons
Resources and Configuration
- 8 drawable icons for widget controls
- Widget metadata file (note_widget_info.xml)
- Widget preview layout for Android 12+ widget picker
- Multi-language strings (English and German)
- Glance Jetpack dependencies version 1.1.1
System Integration
- SyncWorker updates all widgets after sync completion
- NoteEditorViewModel reloads checklist state on resume
- ComposeNoteEditorActivity reflects widget edits
- WebDavSyncService maintains clean connections
- AndroidManifest declares widget receiver and config activity
Complete v1.8.0 Widget Feature Set
- Fully responsive design for phones, tablets and foldables
- Seamless Material Design 3 integration
- Production-ready error handling
- Zero connection leaks
- Immediate UI feedback for all interactions
This commit is contained in:
@@ -162,6 +162,12 @@ dependencies {
|
||||
implementation(libs.androidx.lifecycle.runtime.compose)
|
||||
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// 🆕 v1.8.0: Homescreen Widgets
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
implementation("androidx.glance:glance-appwidget:1.1.1")
|
||||
implementation("androidx.glance:glance-material3:1.1.1")
|
||||
|
||||
// Testing (bleiben so)
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
|
||||
@@ -102,6 +102,25 @@
|
||||
android:foregroundServiceType="dataSync"
|
||||
tools:node="merge" />
|
||||
|
||||
<!-- 🆕 v1.8.0: Widget Receiver -->
|
||||
<receiver
|
||||
android:name=".widget.NoteWidgetReceiver"
|
||||
android:exported="true"
|
||||
android:label="@string/widget_name">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/note_widget_info" />
|
||||
</receiver>
|
||||
|
||||
<!-- 🆕 v1.8.0: Widget Config Activity -->
|
||||
<activity
|
||||
android:name=".widget.NoteWidgetConfigActivity"
|
||||
android:exported="false"
|
||||
android:theme="@style/Theme.SimpleNotes" />
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -5,8 +5,10 @@ import com.thegrizzlylabs.sardineandroid.Sardine
|
||||
import com.thegrizzlylabs.sardineandroid.impl.OkHttpSardine
|
||||
import dev.dettmer.simplenotes.utils.Logger
|
||||
import okhttp3.Credentials
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import java.io.Closeable
|
||||
import java.io.InputStream
|
||||
|
||||
@@ -109,6 +111,73 @@ class SafeSardineWrapper private constructor(
|
||||
return delegate.list(url, depth)
|
||||
}
|
||||
|
||||
/**
|
||||
* ✅ Sichere put()-Implementation mit Response Cleanup
|
||||
*
|
||||
* Im Gegensatz zu OkHttpSardine.put() wird hier der Response-Body garantiert geschlossen.
|
||||
* Verhindert "connection leaked" Warnungen.
|
||||
*/
|
||||
override fun put(url: String, data: ByteArray, contentType: String?) {
|
||||
val mediaType = contentType?.toMediaTypeOrNull()
|
||||
val body = data.toRequestBody(mediaType)
|
||||
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.put(body)
|
||||
.header("Authorization", authHeader)
|
||||
.build()
|
||||
|
||||
okHttpClient.newCall(request).execute().use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
throw java.io.IOException("PUT failed: ${response.code} ${response.message}")
|
||||
}
|
||||
Logger.d(TAG, "put($url) → ${response.code}")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ✅ Sichere delete()-Implementation mit Response Cleanup
|
||||
*
|
||||
* Im Gegensatz zu OkHttpSardine.delete() wird hier der Response-Body garantiert geschlossen.
|
||||
* Verhindert "connection leaked" Warnungen.
|
||||
*/
|
||||
override fun delete(url: String) {
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.delete()
|
||||
.header("Authorization", authHeader)
|
||||
.build()
|
||||
|
||||
okHttpClient.newCall(request).execute().use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
throw java.io.IOException("DELETE failed: ${response.code} ${response.message}")
|
||||
}
|
||||
Logger.d(TAG, "delete($url) → ${response.code}")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ✅ Sichere createDirectory()-Implementation mit Response Cleanup
|
||||
*
|
||||
* Im Gegensatz zu OkHttpSardine.createDirectory() wird hier der Response-Body garantiert geschlossen.
|
||||
* Verhindert "connection leaked" Warnungen.
|
||||
* 405 (Method Not Allowed) wird toleriert da dies bedeutet, dass der Ordner bereits existiert.
|
||||
*/
|
||||
override fun createDirectory(url: String) {
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.method("MKCOL", null)
|
||||
.header("Authorization", authHeader)
|
||||
.build()
|
||||
|
||||
okHttpClient.newCall(request).execute().use { response ->
|
||||
if (!response.isSuccessful && response.code != 405) { // 405 = already exists
|
||||
throw java.io.IOException("MKCOL failed: ${response.code} ${response.message}")
|
||||
}
|
||||
Logger.d(TAG, "createDirectory($url) → ${response.code}")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 🆕 v1.7.2 (IMPL_003): Schließt alle offenen Verbindungen
|
||||
*
|
||||
|
||||
@@ -227,6 +227,20 @@ class SyncWorker(
|
||||
}
|
||||
broadcastSyncCompleted(true, result.syncedCount)
|
||||
|
||||
// 🆕 v1.8.0: Alle Widgets aktualisieren nach Sync
|
||||
try {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Logger.d(TAG, " Updating widgets...")
|
||||
}
|
||||
val glanceManager = androidx.glance.appwidget.GlanceAppWidgetManager(applicationContext)
|
||||
val glanceIds = glanceManager.getGlanceIds(dev.dettmer.simplenotes.widget.NoteWidget::class.java)
|
||||
glanceIds.forEach { id ->
|
||||
dev.dettmer.simplenotes.widget.NoteWidget().update(applicationContext, id)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.w(TAG, "Failed to update widgets: ${e.message}")
|
||||
}
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Logger.d(TAG, "✅ SyncWorker.doWork() SUCCESS")
|
||||
Logger.d(TAG, "═══════════════════════════════════════")
|
||||
|
||||
@@ -1050,6 +1050,7 @@ class WebDavSyncService(private val context: Context) {
|
||||
|
||||
val sardine = SafeSardineWrapper.create(okHttpClient, username, password)
|
||||
|
||||
try {
|
||||
val mdUrl = getMarkdownUrl(serverUrl)
|
||||
|
||||
// Ordner sollte bereits existieren (durch #1.2.1-00), aber Sicherheitscheck
|
||||
@@ -1100,6 +1101,10 @@ class WebDavSyncService(private val context: Context) {
|
||||
}
|
||||
|
||||
return@withContext exportedCount
|
||||
} finally {
|
||||
// 🐛 FIX: Connection Leak — SafeSardineWrapper explizit schließen
|
||||
sardine.close()
|
||||
}
|
||||
}
|
||||
|
||||
private data class DownloadResult(
|
||||
@@ -1717,6 +1722,7 @@ class WebDavSyncService(private val context: Context) {
|
||||
val okHttpClient = OkHttpClient.Builder().build()
|
||||
val sardine = SafeSardineWrapper.create(okHttpClient, username, password)
|
||||
|
||||
try {
|
||||
val mdUrl = getMarkdownUrl(serverUrl)
|
||||
|
||||
// Check if notes-md/ exists
|
||||
@@ -1769,6 +1775,10 @@ class WebDavSyncService(private val context: Context) {
|
||||
|
||||
Logger.d(TAG, "✅ Markdown sync completed: $importedCount imported")
|
||||
importedCount
|
||||
} finally {
|
||||
// 🐛 FIX: Connection Leak — SafeSardineWrapper explizit schließen
|
||||
sardine.close()
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.e(TAG, "Markdown sync failed", e)
|
||||
|
||||
@@ -79,6 +79,18 @@ class ComposeNoteEditorActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 🆕 v1.8.0 (IMPL_025): Reload Checklist-State falls Widget Änderungen gemacht hat.
|
||||
*
|
||||
* Wenn die Activity aus dem Hintergrund zurückkehrt (z.B. nach Widget-Toggle),
|
||||
* wird der aktuelle Note-Stand von Disk geladen und der ViewModel-State
|
||||
* für Checklist-Items aktualisiert.
|
||||
*/
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
viewModel.reloadFromStorage()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -329,6 +329,17 @@ class NoteEditorViewModel(
|
||||
// 🌟 v1.6.0: Trigger onSave Sync
|
||||
triggerOnSaveSync()
|
||||
|
||||
// 🆕 v1.8.0: Betroffene Widgets aktualisieren
|
||||
try {
|
||||
val glanceManager = androidx.glance.appwidget.GlanceAppWidgetManager(getApplication())
|
||||
val glanceIds = glanceManager.getGlanceIds(dev.dettmer.simplenotes.widget.NoteWidget::class.java)
|
||||
glanceIds.forEach { id ->
|
||||
dev.dettmer.simplenotes.widget.NoteWidget().update(getApplication(), id)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.w(TAG, "Failed to update widgets: ${e.message}")
|
||||
}
|
||||
|
||||
_events.emit(NoteEditorEvent.NavigateBack)
|
||||
}
|
||||
}
|
||||
@@ -377,6 +388,41 @@ class NoteEditorViewModel(
|
||||
|
||||
fun canDelete(): Boolean = existingNote != null
|
||||
|
||||
/**
|
||||
* 🆕 v1.8.0 (IMPL_025): Reload Note aus Storage nach Resume
|
||||
*
|
||||
* Wird aufgerufen wenn die Activity aus dem Hintergrund zurückkehrt.
|
||||
* Liest den aktuellen Note-Stand von Disk und aktualisiert den ViewModel-State.
|
||||
*
|
||||
* Wird nur für existierende Checklist-Notes benötigt (neue Notes haben keinen
|
||||
* externen Schreiber). Relevant für Widget-Checklist-Toggles.
|
||||
*
|
||||
* Nur checklistItems werden aktualisiert — nicht title oder content,
|
||||
* damit ungespeicherte Text-Änderungen im Editor nicht verloren gehen.
|
||||
*/
|
||||
fun reloadFromStorage() {
|
||||
val noteId = savedStateHandle.get<String>(ARG_NOTE_ID) ?: return
|
||||
|
||||
val freshNote = storage.loadNote(noteId) ?: return
|
||||
|
||||
// Nur Checklist-Items aktualisieren
|
||||
if (freshNote.noteType == NoteType.CHECKLIST) {
|
||||
val freshItems = freshNote.checklistItems?.sortedBy { it.order }?.map {
|
||||
ChecklistItemState(
|
||||
id = it.id,
|
||||
text = it.text,
|
||||
isChecked = it.isChecked,
|
||||
order = it.order
|
||||
)
|
||||
} ?: return
|
||||
|
||||
_checklistItems.value = sortChecklistItems(freshItems)
|
||||
// existingNote aktualisieren damit beim Speichern der richtige
|
||||
// Basis-State verwendet wird
|
||||
existingNote = freshNote
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// 🌟 v1.6.0: Sync Trigger - onSave
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package dev.dettmer.simplenotes.widget
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.glance.GlanceId
|
||||
import androidx.glance.GlanceTheme
|
||||
import androidx.glance.appwidget.GlanceAppWidget
|
||||
import androidx.glance.appwidget.SizeMode
|
||||
import androidx.glance.appwidget.provideContent
|
||||
import androidx.glance.currentState
|
||||
import androidx.glance.state.PreferencesGlanceStateDefinition
|
||||
import dev.dettmer.simplenotes.storage.NotesStorage
|
||||
|
||||
/**
|
||||
* 🆕 v1.8.0: Homescreen Widget für Notizen und Checklisten
|
||||
*
|
||||
* Unterstützt fünf responsive Größen für breite und schmale Layouts:
|
||||
* - SMALL (110x80dp): Nur Titel
|
||||
* - NARROW_MEDIUM (110x110dp): Schmal + Vorschau / kompakte Checkliste
|
||||
* - NARROW_LARGE (110x250dp): Schmal + voller Inhalt
|
||||
* - WIDE_MEDIUM (250x110dp): Breit + Vorschau
|
||||
* - WIDE_LARGE (250x250dp): Breit + voller Inhalt / interaktive Checkliste
|
||||
*
|
||||
* Features:
|
||||
* - Material You Dynamic Colors
|
||||
* - Interaktive Checklist-Checkboxen
|
||||
* - Sperr-Funktion gegen versehentliches Bearbeiten
|
||||
* - Tap-to-Edit (öffnet NoteEditor)
|
||||
* - Einstellbare Hintergrund-Transparenz
|
||||
* - Permanenter Options-Button (⋮)
|
||||
* - NoteType-differenzierte Icons
|
||||
*/
|
||||
class NoteWidget : GlanceAppWidget() {
|
||||
|
||||
companion object {
|
||||
// Responsive Breakpoints — schmale + breite Spalten
|
||||
val SIZE_SMALL = DpSize(110.dp, 80.dp) // Schmal+kurz: nur Titel
|
||||
val SIZE_NARROW_MEDIUM = DpSize(110.dp, 110.dp) // Schmal+mittel: Vorschau
|
||||
val SIZE_NARROW_LARGE = DpSize(110.dp, 250.dp) // Schmal+groß: voller Inhalt
|
||||
val SIZE_WIDE_MEDIUM = DpSize(250.dp, 110.dp) // Breit+mittel: Vorschau
|
||||
val SIZE_WIDE_LARGE = DpSize(250.dp, 250.dp) // Breit+groß: voller Inhalt
|
||||
}
|
||||
|
||||
override val sizeMode = SizeMode.Responsive(
|
||||
setOf(SIZE_SMALL, SIZE_NARROW_MEDIUM, SIZE_NARROW_LARGE, SIZE_WIDE_MEDIUM, SIZE_WIDE_LARGE)
|
||||
)
|
||||
|
||||
override val stateDefinition = PreferencesGlanceStateDefinition
|
||||
|
||||
override suspend fun provideGlance(context: Context, id: GlanceId) {
|
||||
val storage = NotesStorage(context)
|
||||
|
||||
provideContent {
|
||||
val prefs = currentState<Preferences>()
|
||||
val noteId = prefs[NoteWidgetState.KEY_NOTE_ID]
|
||||
val isLocked = prefs[NoteWidgetState.KEY_IS_LOCKED] ?: false
|
||||
val showOptions = prefs[NoteWidgetState.KEY_SHOW_OPTIONS] ?: false
|
||||
val bgOpacity = prefs[NoteWidgetState.KEY_BACKGROUND_OPACITY] ?: 1.0f
|
||||
|
||||
val note = noteId?.let { nId ->
|
||||
storage.loadNote(nId)
|
||||
}
|
||||
|
||||
GlanceTheme {
|
||||
NoteWidgetContent(
|
||||
note = note,
|
||||
isLocked = isLocked,
|
||||
showOptions = showOptions,
|
||||
bgOpacity = bgOpacity,
|
||||
glanceId = id
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
package dev.dettmer.simplenotes.widget
|
||||
|
||||
import android.content.Context
|
||||
import androidx.glance.GlanceId
|
||||
import androidx.glance.action.ActionParameters
|
||||
import androidx.glance.appwidget.action.ActionCallback
|
||||
import androidx.glance.appwidget.state.updateAppWidgetState
|
||||
import dev.dettmer.simplenotes.models.SyncStatus
|
||||
import dev.dettmer.simplenotes.storage.NotesStorage
|
||||
import dev.dettmer.simplenotes.utils.Logger
|
||||
|
||||
/**
|
||||
* 🆕 v1.8.0: ActionParameter Keys für Widget-Interaktionen
|
||||
*
|
||||
* Shared Keys für alle ActionCallback-Klassen.
|
||||
*/
|
||||
object NoteWidgetActionKeys {
|
||||
val KEY_NOTE_ID = ActionParameters.Key<String>("noteId")
|
||||
val KEY_ITEM_ID = ActionParameters.Key<String>("itemId")
|
||||
val KEY_GLANCE_ID = ActionParameters.Key<String>("glanceId")
|
||||
}
|
||||
|
||||
/**
|
||||
* 🐛 FIX: Checklist-Item abhaken/enthaken
|
||||
*
|
||||
* Top-Level-Klasse (statt nested) für Class.forName()-Kompatibilität.
|
||||
*
|
||||
* - Toggelt isChecked im JSON-File
|
||||
* - Setzt SyncStatus auf PENDING
|
||||
* - Aktualisiert Widget sofort
|
||||
*/
|
||||
class ToggleChecklistItemAction : ActionCallback {
|
||||
companion object {
|
||||
private const val TAG = "ToggleChecklistItem"
|
||||
}
|
||||
|
||||
override suspend fun onAction(
|
||||
context: Context,
|
||||
glanceId: GlanceId,
|
||||
parameters: ActionParameters
|
||||
) {
|
||||
val noteId = parameters[NoteWidgetActionKeys.KEY_NOTE_ID] ?: return
|
||||
val itemId = parameters[NoteWidgetActionKeys.KEY_ITEM_ID] ?: return
|
||||
|
||||
val storage = NotesStorage(context)
|
||||
val note = storage.loadNote(noteId) ?: return
|
||||
|
||||
val updatedItems = note.checklistItems?.map { item ->
|
||||
if (item.id == itemId) {
|
||||
item.copy(isChecked = !item.isChecked)
|
||||
} else item
|
||||
} ?: return
|
||||
|
||||
val updatedNote = note.copy(
|
||||
checklistItems = updatedItems,
|
||||
updatedAt = System.currentTimeMillis(),
|
||||
syncStatus = SyncStatus.PENDING
|
||||
)
|
||||
|
||||
storage.saveNote(updatedNote)
|
||||
Logger.d(TAG, "Toggled checklist item '$itemId' in widget")
|
||||
|
||||
// 🐛 FIX: Glance-State ändern um Re-Render zu erzwingen
|
||||
updateAppWidgetState(context, glanceId) { prefs ->
|
||||
prefs[NoteWidgetState.KEY_LAST_UPDATED] = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
// Widget aktualisieren — Glance erkennt jetzt den State-Change
|
||||
NoteWidget().update(context, glanceId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 🐛 FIX: Widget sperren/entsperren
|
||||
*
|
||||
* Top-Level-Klasse (statt nested) für Class.forName()-Kompatibilität.
|
||||
*/
|
||||
class ToggleLockAction : ActionCallback {
|
||||
override suspend fun onAction(
|
||||
context: Context,
|
||||
glanceId: GlanceId,
|
||||
parameters: ActionParameters
|
||||
) {
|
||||
updateAppWidgetState(context, glanceId) { prefs ->
|
||||
val currentLock = prefs[NoteWidgetState.KEY_IS_LOCKED] ?: false
|
||||
prefs[NoteWidgetState.KEY_IS_LOCKED] = !currentLock
|
||||
// Options ausblenden nach Toggle
|
||||
prefs[NoteWidgetState.KEY_SHOW_OPTIONS] = false
|
||||
}
|
||||
|
||||
NoteWidget().update(context, glanceId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 🐛 FIX: Optionsleiste ein-/ausblenden
|
||||
*
|
||||
* Top-Level-Klasse (statt nested) für Class.forName()-Kompatibilität.
|
||||
*/
|
||||
class ShowOptionsAction : ActionCallback {
|
||||
override suspend fun onAction(
|
||||
context: Context,
|
||||
glanceId: GlanceId,
|
||||
parameters: ActionParameters
|
||||
) {
|
||||
updateAppWidgetState(context, glanceId) { prefs ->
|
||||
val currentShow = prefs[NoteWidgetState.KEY_SHOW_OPTIONS] ?: false
|
||||
prefs[NoteWidgetState.KEY_SHOW_OPTIONS] = !currentShow
|
||||
}
|
||||
|
||||
NoteWidget().update(context, glanceId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 🐛 FIX: Widget-Daten neu laden
|
||||
*
|
||||
* Top-Level-Klasse (statt nested) für Class.forName()-Kompatibilität.
|
||||
*/
|
||||
class RefreshAction : ActionCallback {
|
||||
override suspend fun onAction(
|
||||
context: Context,
|
||||
glanceId: GlanceId,
|
||||
parameters: ActionParameters
|
||||
) {
|
||||
// Options ausblenden
|
||||
updateAppWidgetState(context, glanceId) { prefs ->
|
||||
prefs[NoteWidgetState.KEY_SHOW_OPTIONS] = false
|
||||
}
|
||||
|
||||
NoteWidget().update(context, glanceId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 🆕 v1.8.0: Widget-Konfiguration öffnen (Reconfigure)
|
||||
*
|
||||
* Top-Level-Klasse (statt nested) für Class.forName()-Kompatibilität.
|
||||
* Öffnet die Config-Activity im Reconfigure-Modus.
|
||||
*/
|
||||
class OpenConfigAction : ActionCallback {
|
||||
override suspend fun onAction(
|
||||
context: Context,
|
||||
glanceId: GlanceId,
|
||||
parameters: ActionParameters
|
||||
) {
|
||||
// Options ausblenden
|
||||
updateAppWidgetState(context, glanceId) { prefs ->
|
||||
prefs[NoteWidgetState.KEY_SHOW_OPTIONS] = false
|
||||
}
|
||||
|
||||
// Config-Activity als Reconfigure öffnen
|
||||
val glanceManager = androidx.glance.appwidget.GlanceAppWidgetManager(context)
|
||||
val appWidgetId = glanceManager.getAppWidgetId(glanceId)
|
||||
|
||||
val intent = android.content.Intent(context, NoteWidgetConfigActivity::class.java).apply {
|
||||
putExtra(android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
|
||||
// 🐛 FIX: Eigener Task, damit finish() nicht die MainActivity zeigt
|
||||
flags = android.content.Intent.FLAG_ACTIVITY_NEW_TASK or android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
}
|
||||
context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package dev.dettmer.simplenotes.widget
|
||||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.glance.appwidget.GlanceAppWidgetManager
|
||||
import androidx.glance.appwidget.state.getAppWidgetState
|
||||
import androidx.glance.appwidget.state.updateAppWidgetState
|
||||
import androidx.glance.state.PreferencesGlanceStateDefinition
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import dev.dettmer.simplenotes.storage.NotesStorage
|
||||
import dev.dettmer.simplenotes.ui.theme.SimpleNotesTheme
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* 🆕 v1.8.0: Konfigurations-Activity beim Platzieren eines Widgets
|
||||
*
|
||||
* Zeigt eine Liste aller Notizen. User wählt eine aus,
|
||||
* die dann im Widget angezeigt wird.
|
||||
*
|
||||
* Optionen:
|
||||
* - Notiz auswählen
|
||||
* - Widget initial sperren (optional)
|
||||
* - Hintergrund-Transparenz einstellen
|
||||
*
|
||||
* Unterstützt Reconfiguration (Android 12+): Beim erneuten Öffnen
|
||||
* werden die bestehenden Einstellungen als Defaults geladen.
|
||||
*
|
||||
* 🆕 v1.8.0 (IMPL_025): Auto-Save bei Back-Navigation + Save-FAB
|
||||
*/
|
||||
class NoteWidgetConfigActivity : ComponentActivity() {
|
||||
|
||||
private var appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID
|
||||
|
||||
// 🆕 v1.8.0 (IMPL_025): State-Tracking für Auto-Save bei Back-Navigation
|
||||
private var currentSelectedNoteId: String? = null
|
||||
private var currentLockState: Boolean = false
|
||||
private var currentOpacity: Float = 1.0f
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// Default-Result: Cancelled (falls User zurück-navigiert)
|
||||
setResult(RESULT_CANCELED)
|
||||
|
||||
// 🆕 v1.8.0 (IMPL_025): Auto-Save bei Back-Navigation
|
||||
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
// Auto-Save nur bei Reconfigure (wenn bereits eine Note konfiguriert war)
|
||||
if (currentSelectedNoteId != null) {
|
||||
configureWidget(currentSelectedNoteId!!, currentLockState, currentOpacity)
|
||||
} else {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Widget-ID aus Intent
|
||||
appWidgetId = intent?.extras?.getInt(
|
||||
AppWidgetManager.EXTRA_APPWIDGET_ID,
|
||||
AppWidgetManager.INVALID_APPWIDGET_ID
|
||||
) ?: AppWidgetManager.INVALID_APPWIDGET_ID
|
||||
|
||||
if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
val storage = NotesStorage(this)
|
||||
|
||||
// Bestehende Konfiguration laden (für Reconfigure)
|
||||
lifecycleScope.launch {
|
||||
var existingNoteId: String? = null
|
||||
var existingLock = false
|
||||
var existingOpacity = 1.0f
|
||||
|
||||
try {
|
||||
val glanceId = GlanceAppWidgetManager(this@NoteWidgetConfigActivity)
|
||||
.getGlanceIdBy(appWidgetId)
|
||||
val prefs = getAppWidgetState(
|
||||
this@NoteWidgetConfigActivity,
|
||||
PreferencesGlanceStateDefinition,
|
||||
glanceId
|
||||
)
|
||||
existingNoteId = prefs[NoteWidgetState.KEY_NOTE_ID]
|
||||
existingLock = prefs[NoteWidgetState.KEY_IS_LOCKED] ?: false
|
||||
existingOpacity = prefs[NoteWidgetState.KEY_BACKGROUND_OPACITY] ?: 1.0f
|
||||
} catch (_: Exception) {
|
||||
// Neues Widget — keine bestehende Konfiguration
|
||||
}
|
||||
|
||||
// 🆕 v1.8.0 (IMPL_025): Initiale State-Werte für Auto-Save setzen
|
||||
currentSelectedNoteId = existingNoteId
|
||||
currentLockState = existingLock
|
||||
currentOpacity = existingOpacity
|
||||
|
||||
setContent {
|
||||
SimpleNotesTheme {
|
||||
NoteWidgetConfigScreen(
|
||||
storage = storage,
|
||||
initialLock = existingLock,
|
||||
initialOpacity = existingOpacity,
|
||||
selectedNoteId = existingNoteId,
|
||||
onNoteSelected = { noteId, isLocked, opacity ->
|
||||
configureWidget(noteId, isLocked, opacity)
|
||||
},
|
||||
// 🆕 v1.8.0 (IMPL_025): Save-FAB Callback
|
||||
onSave = { noteId, isLocked, opacity ->
|
||||
configureWidget(noteId, isLocked, opacity)
|
||||
},
|
||||
// 🆕 v1.8.0 (IMPL_025): Settings-Änderungen tracken für Auto-Save
|
||||
onSettingsChanged = { noteId, isLocked, opacity ->
|
||||
currentSelectedNoteId = noteId
|
||||
currentLockState = isLocked
|
||||
currentOpacity = opacity
|
||||
},
|
||||
onCancel = { finish() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun configureWidget(noteId: String, isLocked: Boolean, opacity: Float) {
|
||||
lifecycleScope.launch {
|
||||
val glanceId = GlanceAppWidgetManager(this@NoteWidgetConfigActivity)
|
||||
.getGlanceIdBy(appWidgetId)
|
||||
|
||||
// Widget-State speichern
|
||||
updateAppWidgetState(this@NoteWidgetConfigActivity, glanceId) { prefs ->
|
||||
prefs[NoteWidgetState.KEY_NOTE_ID] = noteId
|
||||
prefs[NoteWidgetState.KEY_IS_LOCKED] = isLocked
|
||||
prefs[NoteWidgetState.KEY_SHOW_OPTIONS] = false
|
||||
prefs[NoteWidgetState.KEY_BACKGROUND_OPACITY] = opacity
|
||||
}
|
||||
|
||||
// Widget initial rendern
|
||||
NoteWidget().update(this@NoteWidgetConfigActivity, glanceId)
|
||||
|
||||
// Erfolg melden
|
||||
val resultIntent = Intent().putExtra(
|
||||
AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId
|
||||
)
|
||||
setResult(RESULT_OK, resultIntent)
|
||||
|
||||
// 🐛 FIX: Zurück zum Homescreen statt zur MainActivity
|
||||
// moveTaskToBack() bringt den Task in den Hintergrund → Homescreen wird sichtbar
|
||||
if (!isTaskRoot) {
|
||||
finish()
|
||||
} else {
|
||||
moveTaskToBack(true)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
package dev.dettmer.simplenotes.widget
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.List
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.outlined.Description
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.dettmer.simplenotes.R
|
||||
import dev.dettmer.simplenotes.models.Note
|
||||
import dev.dettmer.simplenotes.models.NoteType
|
||||
import dev.dettmer.simplenotes.storage.NotesStorage
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
* 🆕 v1.8.0: Compose Screen für Widget-Konfiguration
|
||||
*
|
||||
* Zeigt alle Notizen als auswählbare Liste.
|
||||
* Optionen: Widget-Lock, Hintergrund-Transparenz.
|
||||
* Unterstützt Reconfiguration mit bestehenden Defaults.
|
||||
*
|
||||
* 🆕 v1.8.0 (IMPL_025): Save-FAB + onSettingsChanged für Reconfigure-Flow
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun NoteWidgetConfigScreen(
|
||||
storage: NotesStorage,
|
||||
initialLock: Boolean = false,
|
||||
initialOpacity: Float = 1.0f,
|
||||
selectedNoteId: String? = null,
|
||||
onNoteSelected: (noteId: String, isLocked: Boolean, opacity: Float) -> Unit,
|
||||
onSave: ((noteId: String, isLocked: Boolean, opacity: Float) -> Unit)? = null,
|
||||
onSettingsChanged: ((noteId: String?, isLocked: Boolean, opacity: Float) -> Unit)? = null,
|
||||
onCancel: () -> Unit
|
||||
) {
|
||||
val allNotes = remember { storage.loadAllNotes().sortedByDescending { it.updatedAt } }
|
||||
var lockWidget by remember { mutableStateOf(initialLock) }
|
||||
var opacity by remember { mutableFloatStateOf(initialOpacity) }
|
||||
var currentSelectedId by remember { mutableStateOf(selectedNoteId) }
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.widget_config_title)) }
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
// 🆕 v1.8.0 (IMPL_025): Save-FAB — sichtbar wenn eine Note ausgewählt ist
|
||||
if (currentSelectedId != null) {
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
currentSelectedId?.let { noteId ->
|
||||
onSave?.invoke(noteId, lockWidget, opacity)
|
||||
?: onNoteSelected(noteId, lockWidget, opacity)
|
||||
}
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = stringResource(R.string.widget_config_save)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
) { padding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
) {
|
||||
// Lock-Option
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = stringResource(R.string.widget_lock_label),
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.widget_lock_description),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.outline
|
||||
)
|
||||
}
|
||||
Switch(
|
||||
checked = lockWidget,
|
||||
onCheckedChange = {
|
||||
lockWidget = it
|
||||
// 🆕 v1.8.0 (IMPL_025): Settings-Änderung an Activity melden
|
||||
onSettingsChanged?.invoke(currentSelectedId, lockWidget, opacity)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Opacity-Slider
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 4.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.widget_opacity_label),
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
Text(
|
||||
text = "${(opacity * 100).roundToInt()}%",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = stringResource(R.string.widget_opacity_description),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.outline
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Slider(
|
||||
value = opacity,
|
||||
onValueChange = {
|
||||
opacity = it
|
||||
// 🆕 v1.8.0 (IMPL_025): Settings-Änderung an Activity melden
|
||||
onSettingsChanged?.invoke(currentSelectedId, lockWidget, opacity)
|
||||
},
|
||||
valueRange = 0f..1f,
|
||||
steps = 9
|
||||
)
|
||||
}
|
||||
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
|
||||
|
||||
// Hinweis
|
||||
Text(
|
||||
text = stringResource(R.string.widget_config_hint),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.outline,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp)
|
||||
)
|
||||
|
||||
// Notizen-Liste
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
items(allNotes, key = { it.id }) { note ->
|
||||
NoteSelectionCard(
|
||||
note = note,
|
||||
isSelected = note.id == currentSelectedId,
|
||||
onClick = {
|
||||
currentSelectedId = note.id
|
||||
// 🐛 FIX: Nur auswählen + Settings-Tracking, NICHT sofort konfigurieren
|
||||
onSettingsChanged?.invoke(note.id, lockWidget, opacity)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NoteSelectionCard(
|
||||
note: Note,
|
||||
isSelected: Boolean = false,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp, vertical = 2.dp)
|
||||
.clickable(onClick = onClick),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = if (isSelected) {
|
||||
MaterialTheme.colorScheme.primaryContainer
|
||||
} else {
|
||||
MaterialTheme.colorScheme.surfaceContainerLow
|
||||
}
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = when (note.noteType) {
|
||||
NoteType.TEXT -> Icons.Outlined.Description
|
||||
NoteType.CHECKLIST -> Icons.AutoMirrored.Outlined.List
|
||||
},
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = if (isSelected) {
|
||||
MaterialTheme.colorScheme.onPrimaryContainer
|
||||
} else {
|
||||
MaterialTheme.colorScheme.primary
|
||||
}
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.size(12.dp))
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = note.title.ifEmpty { "Untitled" },
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
maxLines = 1
|
||||
)
|
||||
Text(
|
||||
text = when (note.noteType) {
|
||||
NoteType.TEXT -> note.content.take(50).replace("\n", " ")
|
||||
NoteType.CHECKLIST -> {
|
||||
val items = note.checklistItems ?: emptyList()
|
||||
val checked = items.count { it.isChecked }
|
||||
"✔ $checked/${items.size}"
|
||||
}
|
||||
},
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = if (isSelected) {
|
||||
MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f)
|
||||
} else {
|
||||
MaterialTheme.colorScheme.outline
|
||||
},
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,522 @@
|
||||
package dev.dettmer.simplenotes.widget
|
||||
|
||||
import android.content.ComponentName
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
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.actionParametersOf
|
||||
import androidx.glance.action.actionStartActivity
|
||||
import androidx.glance.action.clickable
|
||||
import androidx.glance.appwidget.CheckBox
|
||||
import androidx.glance.appwidget.action.actionRunCallback
|
||||
import androidx.glance.appwidget.components.CircleIconButton
|
||||
import androidx.glance.appwidget.components.TitleBar
|
||||
import androidx.glance.appwidget.cornerRadius
|
||||
import androidx.glance.appwidget.lazy.LazyColumn
|
||||
import androidx.glance.background
|
||||
import androidx.glance.color.ColorProvider
|
||||
import androidx.glance.layout.Alignment
|
||||
import androidx.glance.layout.Box
|
||||
import androidx.glance.layout.Column
|
||||
import androidx.glance.layout.Row
|
||||
import androidx.glance.layout.Spacer
|
||||
import androidx.glance.layout.fillMaxSize
|
||||
import androidx.glance.layout.fillMaxWidth
|
||||
import androidx.glance.layout.height
|
||||
import androidx.glance.layout.padding
|
||||
import androidx.glance.layout.size
|
||||
import androidx.glance.layout.width
|
||||
import androidx.glance.text.FontWeight
|
||||
import androidx.glance.text.Text
|
||||
import androidx.glance.text.TextStyle
|
||||
import dev.dettmer.simplenotes.R
|
||||
import dev.dettmer.simplenotes.models.Note
|
||||
import dev.dettmer.simplenotes.models.NoteType
|
||||
import dev.dettmer.simplenotes.ui.editor.ComposeNoteEditorActivity
|
||||
|
||||
/**
|
||||
* 🆕 v1.8.0: Glance Composable Content für das Notiz-Widget
|
||||
*
|
||||
* Unterstützt fünf responsive Größenklassen (breit + schmal),
|
||||
* NoteType-Icons, permanenten Options-Button, und einstellbare Opacity.
|
||||
*/
|
||||
|
||||
// ── Size Classification ──
|
||||
|
||||
enum class WidgetSizeClass {
|
||||
SMALL, // Nur Titel
|
||||
NARROW_MED, // Schmal, Vorschau
|
||||
NARROW_TALL, // Schmal, voller Inhalt
|
||||
WIDE_MED, // Breit, Vorschau
|
||||
WIDE_TALL // Breit, voller Inhalt
|
||||
}
|
||||
|
||||
private fun DpSize.toSizeClass(): WidgetSizeClass = when {
|
||||
height < 110.dp -> WidgetSizeClass.SMALL
|
||||
width < 250.dp && height < 250.dp -> WidgetSizeClass.NARROW_MED
|
||||
width < 250.dp -> WidgetSizeClass.NARROW_TALL
|
||||
height < 250.dp -> WidgetSizeClass.WIDE_MED
|
||||
else -> WidgetSizeClass.WIDE_TALL
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NoteWidgetContent(
|
||||
note: Note?,
|
||||
isLocked: Boolean,
|
||||
showOptions: Boolean,
|
||||
bgOpacity: Float,
|
||||
glanceId: GlanceId
|
||||
) {
|
||||
val size = LocalSize.current
|
||||
val context = LocalContext.current
|
||||
val sizeClass = size.toSizeClass()
|
||||
|
||||
if (note == null) {
|
||||
EmptyWidgetContent(bgOpacity)
|
||||
return
|
||||
}
|
||||
|
||||
// Background mit Opacity
|
||||
val bgModifier = if (bgOpacity < 1.0f) {
|
||||
GlanceModifier.background(
|
||||
ColorProvider(
|
||||
day = Color.White.copy(alpha = bgOpacity),
|
||||
night = Color(0xFF1C1B1F).copy(alpha = bgOpacity)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
GlanceModifier.background(GlanceTheme.colors.widgetBackground)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = GlanceModifier
|
||||
.fillMaxSize()
|
||||
.cornerRadius(16.dp)
|
||||
.then(bgModifier)
|
||||
) {
|
||||
Column(modifier = GlanceModifier.fillMaxSize()) {
|
||||
// 🆕 v1.8.0 (IMPL_025): Offizielle TitleBar mit CircleIconButton (48dp Hit Area)
|
||||
TitleBar(
|
||||
startIcon = ImageProvider(
|
||||
when {
|
||||
isLocked -> R.drawable.ic_lock
|
||||
note.noteType == NoteType.CHECKLIST -> R.drawable.ic_widget_checklist
|
||||
else -> R.drawable.ic_note
|
||||
}
|
||||
),
|
||||
title = note.title.ifEmpty { "Untitled" },
|
||||
iconColor = GlanceTheme.colors.onSurface,
|
||||
textColor = GlanceTheme.colors.onSurface,
|
||||
actions = {
|
||||
CircleIconButton(
|
||||
imageProvider = ImageProvider(R.drawable.ic_more_vert),
|
||||
contentDescription = "Options",
|
||||
backgroundColor = null, // Transparent → nur Icon + 48x48dp Hit Area
|
||||
contentColor = GlanceTheme.colors.onSurface,
|
||||
onClick = actionRunCallback<ShowOptionsAction>(
|
||||
actionParametersOf(
|
||||
NoteWidgetActionKeys.KEY_GLANCE_ID to glanceId.toString()
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
// Optionsleiste (ein-/ausblendbar)
|
||||
if (showOptions) {
|
||||
OptionsBar(
|
||||
isLocked = isLocked,
|
||||
noteId = note.id,
|
||||
glanceId = glanceId
|
||||
)
|
||||
}
|
||||
|
||||
// Content-Bereich — Click öffnet Editor (unlocked) oder Options (locked)
|
||||
val contentClickModifier = GlanceModifier
|
||||
.fillMaxSize()
|
||||
.clickable(
|
||||
onClick = if (!isLocked) {
|
||||
actionStartActivity(
|
||||
ComponentName(context, ComposeNoteEditorActivity::class.java),
|
||||
actionParametersOf(
|
||||
androidx.glance.action.ActionParameters.Key<String>("extra_note_id") to note.id
|
||||
)
|
||||
)
|
||||
} else {
|
||||
actionRunCallback<ShowOptionsAction>(
|
||||
actionParametersOf(
|
||||
NoteWidgetActionKeys.KEY_GLANCE_ID to glanceId.toString()
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
// Content — abhängig von SizeClass
|
||||
when (sizeClass) {
|
||||
WidgetSizeClass.SMALL -> {
|
||||
// Nur TitleBar, leerer Body als Click-Target
|
||||
Box(modifier = contentClickModifier) {}
|
||||
}
|
||||
|
||||
WidgetSizeClass.NARROW_MED -> Box(modifier = contentClickModifier) {
|
||||
when (note.noteType) {
|
||||
NoteType.TEXT -> TextNotePreview(note, compact = true)
|
||||
NoteType.CHECKLIST -> ChecklistCompactView(
|
||||
note = note,
|
||||
maxItems = 2,
|
||||
isLocked = isLocked,
|
||||
glanceId = glanceId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
WidgetSizeClass.NARROW_TALL -> Box(modifier = contentClickModifier) {
|
||||
when (note.noteType) {
|
||||
NoteType.TEXT -> TextNoteFullView(note)
|
||||
NoteType.CHECKLIST -> ChecklistFullView(
|
||||
note = note,
|
||||
isLocked = isLocked,
|
||||
glanceId = glanceId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
WidgetSizeClass.WIDE_MED -> Box(modifier = contentClickModifier) {
|
||||
when (note.noteType) {
|
||||
NoteType.TEXT -> TextNotePreview(note, compact = false)
|
||||
NoteType.CHECKLIST -> ChecklistCompactView(
|
||||
note = note,
|
||||
maxItems = 3,
|
||||
isLocked = isLocked,
|
||||
glanceId = glanceId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
WidgetSizeClass.WIDE_TALL -> Box(modifier = contentClickModifier) {
|
||||
when (note.noteType) {
|
||||
NoteType.TEXT -> TextNoteFullView(note)
|
||||
NoteType.CHECKLIST -> ChecklistFullView(
|
||||
note = note,
|
||||
isLocked = isLocked,
|
||||
glanceId = glanceId
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Optionsleiste — Lock/Unlock + Refresh + Open in App
|
||||
*/
|
||||
@Composable
|
||||
private fun OptionsBar(
|
||||
isLocked: Boolean,
|
||||
noteId: String,
|
||||
glanceId: GlanceId
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
Row(
|
||||
modifier = GlanceModifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp, vertical = 6.dp)
|
||||
.background(GlanceTheme.colors.secondaryContainer),
|
||||
horizontalAlignment = Alignment.End,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Lock/Unlock Toggle
|
||||
Image(
|
||||
provider = ImageProvider(
|
||||
if (isLocked) R.drawable.ic_lock_open else R.drawable.ic_lock
|
||||
),
|
||||
contentDescription = if (isLocked) "Unlock" else "Lock",
|
||||
modifier = GlanceModifier
|
||||
.size(36.dp)
|
||||
.padding(6.dp)
|
||||
.clickable(
|
||||
onClick = actionRunCallback<ToggleLockAction>(
|
||||
actionParametersOf(
|
||||
NoteWidgetActionKeys.KEY_GLANCE_ID to glanceId.toString()
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
Spacer(modifier = GlanceModifier.width(4.dp))
|
||||
|
||||
// Refresh
|
||||
Image(
|
||||
provider = ImageProvider(R.drawable.ic_refresh),
|
||||
contentDescription = "Refresh",
|
||||
modifier = GlanceModifier
|
||||
.size(36.dp)
|
||||
.padding(6.dp)
|
||||
.clickable(
|
||||
onClick = actionRunCallback<RefreshAction>(
|
||||
actionParametersOf(
|
||||
NoteWidgetActionKeys.KEY_GLANCE_ID to glanceId.toString()
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
Spacer(modifier = GlanceModifier.width(4.dp))
|
||||
|
||||
// Settings (Reconfigure)
|
||||
Image(
|
||||
provider = ImageProvider(R.drawable.ic_settings),
|
||||
contentDescription = "Settings",
|
||||
modifier = GlanceModifier
|
||||
.size(36.dp)
|
||||
.padding(6.dp)
|
||||
.clickable(
|
||||
onClick = actionRunCallback<OpenConfigAction>(
|
||||
actionParametersOf(
|
||||
NoteWidgetActionKeys.KEY_GLANCE_ID to glanceId.toString()
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
Spacer(modifier = GlanceModifier.width(4.dp))
|
||||
|
||||
// Open in App
|
||||
Image(
|
||||
provider = ImageProvider(R.drawable.ic_open_in_new),
|
||||
contentDescription = "Open",
|
||||
modifier = GlanceModifier
|
||||
.size(36.dp)
|
||||
.padding(6.dp)
|
||||
.clickable(
|
||||
onClick = actionStartActivity(
|
||||
ComponentName(context, ComposeNoteEditorActivity::class.java),
|
||||
actionParametersOf(
|
||||
androidx.glance.action.ActionParameters.Key<String>("extra_note_id") to noteId
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Text Note Views ──
|
||||
|
||||
@Composable
|
||||
private fun TextNotePreview(note: Note, compact: Boolean) {
|
||||
Text(
|
||||
text = note.content.take(if (compact) 100 else 200),
|
||||
style = TextStyle(
|
||||
color = GlanceTheme.colors.onSurface,
|
||||
fontSize = if (compact) 13.sp else 14.sp
|
||||
),
|
||||
maxLines = if (compact) 2 else 3,
|
||||
modifier = GlanceModifier.padding(horizontal = 12.dp, vertical = 4.dp)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TextNoteFullView(note: Note) {
|
||||
LazyColumn(
|
||||
modifier = GlanceModifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 12.dp)
|
||||
) {
|
||||
val paragraphs = note.content.split("\n").filter { it.isNotBlank() }
|
||||
items(paragraphs.size) { index ->
|
||||
Text(
|
||||
text = paragraphs[index],
|
||||
style = TextStyle(
|
||||
color = GlanceTheme.colors.onSurface,
|
||||
fontSize = 14.sp
|
||||
),
|
||||
modifier = GlanceModifier.padding(bottom = 4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Checklist Views ──
|
||||
|
||||
/**
|
||||
* Kompakte Checklist-Ansicht für MEDIUM-Größen.
|
||||
* Zeigt maxItems interaktive Checkboxen + Zusammenfassung.
|
||||
*/
|
||||
@Composable
|
||||
private fun ChecklistCompactView(
|
||||
note: Note,
|
||||
maxItems: Int,
|
||||
isLocked: Boolean,
|
||||
glanceId: GlanceId
|
||||
) {
|
||||
val items = note.checklistItems?.sortedBy { it.order } ?: return
|
||||
val visibleItems = items.take(maxItems)
|
||||
val remainingCount = items.size - visibleItems.size
|
||||
val checkedCount = items.count { it.isChecked }
|
||||
|
||||
Column(modifier = GlanceModifier.padding(horizontal = 8.dp, vertical = 2.dp)) {
|
||||
visibleItems.forEach { item ->
|
||||
if (isLocked) {
|
||||
Row(
|
||||
modifier = GlanceModifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 2.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = if (item.isChecked) "✅" else "☐",
|
||||
style = TextStyle(fontSize = 14.sp)
|
||||
)
|
||||
Spacer(modifier = GlanceModifier.width(6.dp))
|
||||
Text(
|
||||
text = item.text,
|
||||
style = TextStyle(
|
||||
color = if (item.isChecked) GlanceTheme.colors.outline
|
||||
else GlanceTheme.colors.onSurface,
|
||||
fontSize = 13.sp
|
||||
),
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
} else {
|
||||
CheckBox(
|
||||
checked = item.isChecked,
|
||||
onCheckedChange = actionRunCallback<ToggleChecklistItemAction>(
|
||||
actionParametersOf(
|
||||
NoteWidgetActionKeys.KEY_NOTE_ID to note.id,
|
||||
NoteWidgetActionKeys.KEY_ITEM_ID to item.id,
|
||||
NoteWidgetActionKeys.KEY_GLANCE_ID to glanceId.toString()
|
||||
)
|
||||
),
|
||||
text = item.text,
|
||||
style = TextStyle(
|
||||
color = GlanceTheme.colors.onSurface,
|
||||
fontSize = 13.sp
|
||||
),
|
||||
modifier = GlanceModifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 1.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (remainingCount > 0) {
|
||||
Text(
|
||||
text = "+$remainingCount more · ✔ $checkedCount/${items.size}",
|
||||
style = TextStyle(
|
||||
color = GlanceTheme.colors.outline,
|
||||
fontSize = 12.sp
|
||||
),
|
||||
modifier = GlanceModifier.padding(top = 2.dp, start = 4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vollständige Checklist-Ansicht für LARGE-Größen.
|
||||
*/
|
||||
@Composable
|
||||
private fun ChecklistFullView(
|
||||
note: Note,
|
||||
isLocked: Boolean,
|
||||
glanceId: GlanceId
|
||||
) {
|
||||
val items = note.checklistItems?.sortedBy { it.order } ?: return
|
||||
|
||||
LazyColumn(
|
||||
modifier = GlanceModifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 8.dp)
|
||||
) {
|
||||
items(items.size) { index ->
|
||||
val item = items[index]
|
||||
|
||||
if (isLocked) {
|
||||
Row(
|
||||
modifier = GlanceModifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 2.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = if (item.isChecked) "✅" else "☐",
|
||||
style = TextStyle(fontSize = 16.sp)
|
||||
)
|
||||
Spacer(modifier = GlanceModifier.width(8.dp))
|
||||
Text(
|
||||
text = item.text,
|
||||
style = TextStyle(
|
||||
color = GlanceTheme.colors.onSurface,
|
||||
fontSize = 14.sp
|
||||
),
|
||||
maxLines = 2
|
||||
)
|
||||
}
|
||||
} else {
|
||||
CheckBox(
|
||||
checked = item.isChecked,
|
||||
onCheckedChange = actionRunCallback<ToggleChecklistItemAction>(
|
||||
actionParametersOf(
|
||||
NoteWidgetActionKeys.KEY_NOTE_ID to note.id,
|
||||
NoteWidgetActionKeys.KEY_ITEM_ID to item.id,
|
||||
NoteWidgetActionKeys.KEY_GLANCE_ID to glanceId.toString()
|
||||
)
|
||||
),
|
||||
text = item.text,
|
||||
style = TextStyle(
|
||||
color = GlanceTheme.colors.onSurface,
|
||||
fontSize = 14.sp
|
||||
),
|
||||
modifier = GlanceModifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 1.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Empty State ──
|
||||
|
||||
@Composable
|
||||
private fun EmptyWidgetContent(bgOpacity: Float) {
|
||||
val bgModifier = if (bgOpacity < 1.0f) {
|
||||
GlanceModifier.background(
|
||||
ColorProvider(
|
||||
day = Color.White.copy(alpha = bgOpacity),
|
||||
night = Color(0xFF1C1B1F).copy(alpha = bgOpacity)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
GlanceModifier.background(GlanceTheme.colors.widgetBackground)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = GlanceModifier
|
||||
.fillMaxSize()
|
||||
.cornerRadius(16.dp)
|
||||
.then(bgModifier)
|
||||
.padding(16.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "Note not found",
|
||||
style = TextStyle(
|
||||
color = GlanceTheme.colors.outline,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package dev.dettmer.simplenotes.widget
|
||||
|
||||
import androidx.glance.appwidget.GlanceAppWidgetReceiver
|
||||
|
||||
/**
|
||||
* 🆕 v1.8.0: BroadcastReceiver für das Notiz-Widget
|
||||
*
|
||||
* Muss im AndroidManifest.xml registriert werden.
|
||||
* Delegiert alle Widget-Updates an NoteWidget.
|
||||
*/
|
||||
class NoteWidgetReceiver : GlanceAppWidgetReceiver() {
|
||||
override val glanceAppWidget: NoteWidget = NoteWidget()
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package dev.dettmer.simplenotes.widget
|
||||
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.floatPreferencesKey
|
||||
import androidx.datastore.preferences.core.longPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
|
||||
/**
|
||||
* 🆕 v1.8.0: Widget-State Keys (per Widget-Instance)
|
||||
*
|
||||
* Gespeichert via PreferencesGlanceStateDefinition (DataStore).
|
||||
* Jede Widget-Instanz hat eigene Preferences.
|
||||
*/
|
||||
object NoteWidgetState {
|
||||
/** ID der angezeigten Notiz */
|
||||
val KEY_NOTE_ID = stringPreferencesKey("widget_note_id")
|
||||
|
||||
/** Ob das Widget gesperrt ist (keine Bearbeitung möglich) */
|
||||
val KEY_IS_LOCKED = booleanPreferencesKey("widget_is_locked")
|
||||
|
||||
/** Ob die Optionsleiste angezeigt wird */
|
||||
val KEY_SHOW_OPTIONS = booleanPreferencesKey("widget_show_options")
|
||||
|
||||
/** Hintergrund-Transparenz (0.0 = vollständig transparent, 1.0 = opak) */
|
||||
val KEY_BACKGROUND_OPACITY = floatPreferencesKey("widget_bg_opacity")
|
||||
|
||||
/** Timestamp des letzten Updates — erzwingt Widget-Recomposition */
|
||||
val KEY_LAST_UPDATED = longPreferencesKey("widget_last_updated")
|
||||
}
|
||||
9
android/app/src/main/res/drawable/ic_lock.xml
Normal file
9
android/app/src/main/res/drawable/ic_lock.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM12,17c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM15.1,8L8.9,8L8.9,6c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2z"/>
|
||||
</vector>
|
||||
9
android/app/src/main/res/drawable/ic_lock_open.xml
Normal file
9
android/app/src/main/res/drawable/ic_lock_open.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,17c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6h1.9c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM18,20L6,20L6,10h12v10z"/>
|
||||
</vector>
|
||||
9
android/app/src/main/res/drawable/ic_more_vert.xml
Normal file
9
android/app/src/main/res/drawable/ic_more_vert.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z"/>
|
||||
</vector>
|
||||
9
android/app/src/main/res/drawable/ic_note.xml
Normal file
9
android/app/src/main/res/drawable/ic_note.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M14,2L6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6zM16,18L8,18v-2h8v2zM16,14L8,14v-2h8v2zM13,9L13,3.5L18.5,9L13,9z"/>
|
||||
</vector>
|
||||
9
android/app/src/main/res/drawable/ic_open_in_new.xml
Normal file
9
android/app/src/main/res/drawable/ic_open_in_new.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M19,19H5V5h7V3H5c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2v-7h-2v7zM14,3v2h3.59l-9.83,9.83 1.41,1.41L19,6.41V10h2V3h-7z"/>
|
||||
</vector>
|
||||
9
android/app/src/main/res/drawable/ic_refresh.xml
Normal file
9
android/app/src/main/res/drawable/ic_refresh.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"/>
|
||||
</vector>
|
||||
9
android/app/src/main/res/drawable/ic_settings.xml
Normal file
9
android/app/src/main/res/drawable/ic_settings.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M22,7h-9v2h9V7zM22,15h-9v2h9V15zM5.54,11L2,7.46l1.41,-1.41l2.12,2.12l4.24,-4.24l1.41,1.41L5.54,11zM5.54,19L2,15.46l1.41,-1.41l2.12,2.12l4.24,-4.24l1.41,1.41L5.54,19z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="?android:attr/colorBackground" />
|
||||
<corners android:radius="16dp" />
|
||||
</shape>
|
||||
58
android/app/src/main/res/layout/widget_preview.xml
Normal file
58
android/app/src/main/res/layout/widget_preview.xml
Normal file
@@ -0,0 +1,58 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Widget Preview Layout for Android 12+ widget picker (previewLayout) -->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp"
|
||||
android:background="@drawable/widget_preview_background"
|
||||
android:clipToOutline="true">
|
||||
|
||||
<!-- TitleBar Simulation -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:src="@drawable/ic_note_24"
|
||||
android:importantForAccessibility="no" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginStart="8dp"
|
||||
android:text="@string/widget_preview_title"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end" />
|
||||
|
||||
<ImageView
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:src="@drawable/ic_more_vert"
|
||||
android:importantForAccessibility="no"
|
||||
android:alpha="0.5" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Content Preview -->
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/widget_preview_content"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:maxLines="5"
|
||||
android:ellipsize="end"
|
||||
android:lineSpacingMultiplier="1.3" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -511,4 +511,20 @@
|
||||
<item quantity="one">%d erledigt</item>
|
||||
<item quantity="other">%d erledigt</item>
|
||||
</plurals>
|
||||
|
||||
<!-- ============================= -->
|
||||
<!-- WIDGETS v1.8.0 -->
|
||||
<!-- ============================= -->
|
||||
<string name="widget_name">Simple Notes</string>
|
||||
<string name="widget_description">Zeige eine Notiz oder Checkliste auf dem Startbildschirm</string>
|
||||
<string name="widget_config_title">Notiz auswählen</string>
|
||||
<string name="widget_config_hint">Tippe auf eine Notiz, um sie als Widget hinzuzufügen</string>
|
||||
<string name="widget_config_save">Speichern</string>
|
||||
<string name="widget_lock_label">Widget sperren</string>
|
||||
<string name="widget_lock_description">Versehentliches Bearbeiten verhindern</string>
|
||||
<string name="widget_note_not_found">Notiz nicht gefunden</string>
|
||||
<string name="widget_opacity_label">Hintergrund-Transparenz</string>
|
||||
<string name="widget_opacity_description">Transparenz des Widget-Hintergrunds anpassen</string>
|
||||
<string name="widget_preview_title">Einkaufsliste</string>
|
||||
<string name="widget_preview_content">Milch, Eier, Brot, Butter, Käse, Tomaten, Nudeln, Reis, Olivenöl…</string>
|
||||
</resources>
|
||||
|
||||
@@ -530,4 +530,20 @@
|
||||
<string name="sync_parallel_downloads_desc_7">Fast (7x faster)</string>
|
||||
<string name="sync_parallel_downloads_desc_10">Maximum (10x faster, may stress server)</string>
|
||||
|
||||
<!-- ============================= -->
|
||||
<!-- WIDGETS v1.8.0 -->
|
||||
<!-- ============================= -->
|
||||
<string name="widget_name">Simple Notes</string>
|
||||
<string name="widget_description">Display a note or checklist on your home screen</string>
|
||||
<string name="widget_config_title">Choose a Note</string>
|
||||
<string name="widget_config_hint">Tap a note to add it as a widget</string>
|
||||
<string name="widget_config_save">Save</string>
|
||||
<string name="widget_lock_label">Lock widget</string>
|
||||
<string name="widget_lock_description">Prevent accidental edits</string>
|
||||
<string name="widget_note_not_found">Note not found</string>
|
||||
<string name="widget_opacity_label">Background opacity</string>
|
||||
<string name="widget_opacity_description">Adjust the transparency of the widget background</string>
|
||||
<string name="widget_preview_title">Shopping List</string>
|
||||
<string name="widget_preview_content">Milk, eggs, bread, butter, cheese, tomatoes, pasta, rice, olive oil…</string>
|
||||
|
||||
</resources>
|
||||
|
||||
20
android/app/src/main/res/xml/note_widget_info.xml
Normal file
20
android/app/src/main/res/xml/note_widget_info.xml
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 🆕 v1.8.0: Widget metadata for homescreen note/checklist widget -->
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:description="@string/widget_description"
|
||||
android:initialLayout="@layout/glance_default_loading_layout"
|
||||
android:minWidth="180dp"
|
||||
android:minHeight="110dp"
|
||||
android:minResizeWidth="110dp"
|
||||
android:minResizeHeight="80dp"
|
||||
android:maxResizeWidth="530dp"
|
||||
android:maxResizeHeight="530dp"
|
||||
android:targetCellWidth="3"
|
||||
android:targetCellHeight="2"
|
||||
android:resizeMode="horizontal|vertical"
|
||||
android:widgetCategory="home_screen"
|
||||
android:updatePeriodMillis="1800000"
|
||||
android:configure="dev.dettmer.simplenotes.widget.NoteWidgetConfigActivity"
|
||||
android:previewImage="@layout/widget_preview"
|
||||
android:previewLayout="@layout/widget_preview"
|
||||
android:widgetFeatures="reconfigurable" />
|
||||
Reference in New Issue
Block a user