diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 111a0dc01..3b1cd56f6 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -159,6 +159,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{
updateSingleNoteWidgets(context);
updateNoteListWidgets(context);
+ updateInteractiveNoteListWidgets(context);
});
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/WidgetNotesListDao.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/WidgetNotesListDao.java
index 1f34e9278..435c1fd36 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/WidgetNotesListDao.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/WidgetNotesListDao.java
@@ -8,6 +8,7 @@
import androidx.room.Dao;
import androidx.room.Insert;
+import androidx.room.OnConflictStrategy;
import androidx.room.Query;
import it.niedermann.owncloud.notes.persistence.entity.NotesListWidgetData;
@@ -15,7 +16,7 @@
@Dao
public interface WidgetNotesListDao {
- @Insert
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
void createOrUpdateNoteListWidgetData(NotesListWidgetData data);
@Query("DELETE FROM NOTESLISTWIDGETDATA WHERE id = :appWidgetId")
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/NoteUtil.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/NoteUtil.java
index e6d8110e6..a93b99c4f 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/NoteUtil.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/NoteUtil.java
@@ -85,8 +85,10 @@ public static String generateNoteExcerpt(@NonNull String content, @Nullable Stri
if (!TextUtils.isEmpty(title)) {
final String trimmedTitle = removeMarkdown(replaceCheckboxesWithEmojis(title.trim()));
- if (result.startsWith(trimmedTitle)) {
- result = result.substring(trimmedTitle.length());
+ final int firstLineEnd = result.indexOf('\n');
+ final String firstLine = firstLineEnd < 0 ? result : result.substring(0, firstLineEnd);
+ if (firstLine.equals(trimmedTitle)) {
+ result = firstLineEnd < 0 ? "" : result.substring(firstLineEnd + 1);
}
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/widget/interactivelist/InteractiveNoteListWidget.kt b/app/src/main/java/it/niedermann/owncloud/notes/widget/interactivelist/InteractiveNoteListWidget.kt
new file mode 100644
index 000000000..332f823c4
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/widget/interactivelist/InteractiveNoteListWidget.kt
@@ -0,0 +1,149 @@
+/*
+ * Nextcloud Notes - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+package it.niedermann.owncloud.notes.widget.interactivelist
+
+import android.app.PendingIntent
+import android.appwidget.AppWidgetManager
+import android.appwidget.AppWidgetProvider
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.res.ColorStateList
+import android.os.Build
+import android.os.Bundle
+import android.widget.RemoteViews
+import androidx.core.net.toUri
+import it.niedermann.owncloud.notes.R
+import it.niedermann.owncloud.notes.branding.BrandingUtil
+import it.niedermann.owncloud.notes.edit.EditNoteActivity
+import it.niedermann.owncloud.notes.persistence.NotesRepository
+import it.niedermann.owncloud.notes.persistence.entity.NotesListWidgetData
+import it.niedermann.owncloud.notes.shared.model.ENavigationCategoryType
+import it.niedermann.owncloud.notes.shared.model.NavigationCategory
+import it.niedermann.owncloud.notes.shared.util.WidgetUtil
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+
+class InteractiveNoteListWidget : AppWidgetProvider() {
+ private val executor: ExecutorService = Executors.newCachedThreadPool()
+
+ override fun onUpdate(
+ context: Context,
+ appWidgetManager: AppWidgetManager,
+ appWidgetIds: IntArray
+ ) {
+ super.onUpdate(context, appWidgetManager, appWidgetIds)
+ updateAppWidget(context, appWidgetManager, appWidgetIds)
+ }
+
+ override fun onDeleted(context: Context, appWidgetIds: IntArray) {
+ super.onDeleted(context, appWidgetIds)
+ val repo = NotesRepository.getInstance(context)
+
+ for (appWidgetId in appWidgetIds) {
+ executor.execute {
+ repo.removeNoteListWidget(appWidgetId)
+ InteractiveWidgetPreferences.remove(context, appWidgetId)
+ }
+ }
+ }
+
+ companion object {
+ fun updateAppWidget(context: Context, awm: AppWidgetManager, appWidgetIds: IntArray) {
+ val repo = NotesRepository.getInstance(context)
+ appWidgetIds.forEach { appWidgetId ->
+ val data = repo.getNoteListWidgetData(appWidgetId) ?: return@forEach
+ updateSingleWidget(context, awm, appWidgetId, data)
+ }
+ }
+
+ private fun updateSingleWidget(
+ context: Context,
+ awm: AppWidgetManager,
+ appWidgetId: Int,
+ data: NotesListWidgetData
+ ) {
+ val serviceIntent = Intent(context, InteractiveNoteListWidgetService::class.java).apply {
+ putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
+ setData(toUri(Intent.URI_INTENT_SCHEME).toUri())
+ }
+
+ val openTemplateIntent = Intent(context, EditNoteActivity::class.java).apply {
+ setPackage(context.packageName)
+ }
+ val openTemplateFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
+ } else {
+ PendingIntent.FLAG_UPDATE_CURRENT
+ }
+ val openTemplatePendingIntent =
+ PendingIntent.getActivity(context, 0, openTemplateIntent, openTemplateFlags)
+
+ val createNotePendingIntent =
+ PendingIntent.getActivity(
+ context,
+ appWidgetId,
+ createNoteIntent(context, data),
+ WidgetUtil.pendingIntentFlagCompat(PendingIntent.FLAG_UPDATE_CURRENT)
+ )
+
+ val views = RemoteViews(context.packageName, R.layout.widget_interactive_note_list).apply {
+ setRemoteAdapter(R.id.interactive_note_list_lv, serviceIntent)
+ setPendingIntentTemplate(R.id.interactive_note_list_lv, openTemplatePendingIntent)
+ setEmptyView(
+ R.id.interactive_note_list_lv,
+ R.id.interactive_note_list_placeholder_tv
+ )
+ setOnClickPendingIntent(R.id.interactive_create_note_button, createNotePendingIntent)
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ setColorStateList(
+ R.id.interactive_create_note_button,
+ "setBackgroundTintList",
+ ColorStateList.valueOf(BrandingUtil.readBrandMainColor(context))
+ )
+ }
+ }
+
+ awm.run {
+ updateAppWidget(appWidgetId, views)
+ notifyAppWidgetViewDataChanged(intArrayOf(appWidgetId), R.id.interactive_note_list_lv)
+ }
+ }
+
+ private fun createNoteIntent(context: Context, data: NotesListWidgetData): Intent {
+ val navigationCategory = if (data.mode == NotesListWidgetData.MODE_DISPLAY_STARRED) {
+ NavigationCategory(ENavigationCategoryType.FAVORITES)
+ } else {
+ NavigationCategory(data.accountId, data.category)
+ }
+
+ val bundle = Bundle().apply {
+ putSerializable(EditNoteActivity.PARAM_CATEGORY, navigationCategory)
+ putLong(EditNoteActivity.PARAM_ACCOUNT_ID, data.accountId)
+ }
+
+ return Intent(context, EditNoteActivity::class.java).apply {
+ setPackage(context.packageName)
+ putExtras(bundle)
+ setData("interactive-create://${data.id}".toUri())
+ }
+ }
+
+ @JvmStatic
+ fun updateInteractiveNoteListWidgets(context: Context) {
+ val appWidgetIds = AppWidgetManager.getInstance(context)
+ .getAppWidgetIds(ComponentName(context, InteractiveNoteListWidget::class.java))
+
+ val intent = Intent(context, InteractiveNoteListWidget::class.java).apply {
+ action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
+ putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds)
+ }
+ context.sendBroadcast(intent)
+ }
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/widget/interactivelist/InteractiveNoteListWidgetConfigurationActivity.kt b/app/src/main/java/it/niedermann/owncloud/notes/widget/interactivelist/InteractiveNoteListWidgetConfigurationActivity.kt
new file mode 100644
index 000000000..ef30cb03b
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/widget/interactivelist/InteractiveNoteListWidgetConfigurationActivity.kt
@@ -0,0 +1,229 @@
+/*
+ * Nextcloud Notes - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+package it.niedermann.owncloud.notes.widget.interactivelist
+
+import android.app.Activity
+import android.appwidget.AppWidgetManager
+import android.content.Intent
+import android.os.Bundle
+import android.util.Log
+import android.widget.Toast
+import androidx.lifecycle.ViewModelProvider
+import com.nextcloud.android.sso.exceptions.SSOException
+import com.nextcloud.android.sso.helper.SingleAccountHelper
+import it.niedermann.owncloud.notes.LockedActivity
+import it.niedermann.owncloud.notes.NotesApplication
+import it.niedermann.owncloud.notes.R
+import it.niedermann.owncloud.notes.branding.BrandingUtil
+import it.niedermann.owncloud.notes.databinding.ActivityInteractiveWidgetConfigurationBinding
+import it.niedermann.owncloud.notes.main.MainActivity
+import it.niedermann.owncloud.notes.main.navigation.NavigationAdapter
+import it.niedermann.owncloud.notes.main.navigation.NavigationClickListener
+import it.niedermann.owncloud.notes.main.navigation.NavigationItem
+import it.niedermann.owncloud.notes.persistence.NotesRepository
+import it.niedermann.owncloud.notes.persistence.entity.Account
+import it.niedermann.owncloud.notes.persistence.entity.NotesListWidgetData
+import it.niedermann.owncloud.notes.shared.model.ENavigationCategoryType
+import it.niedermann.owncloud.notes.widget.notelist.NoteListViewModel
+import java.util.concurrent.Executors
+
+class InteractiveNoteListWidgetConfigurationActivity : LockedActivity(), NavigationClickListener {
+ private val executor = Executors.newCachedThreadPool()
+
+ private var binding: ActivityInteractiveWidgetConfigurationBinding? = null
+ private lateinit var viewModel: NoteListViewModel
+ private lateinit var adapterCategories: NavigationAdapter
+ private lateinit var repo: NotesRepository
+
+ private var appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID
+ private var localAccount: Account? = null
+ private var selectedItemId = MainActivity.ADAPTER_KEY_RECENT
+ private var selectedItem: NavigationItem? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setResult(Activity.RESULT_CANCELED)
+
+ repo = NotesRepository.getInstance(this)
+ appWidgetId = intent.extras?.getInt(
+ AppWidgetManager.EXTRA_APPWIDGET_ID,
+ AppWidgetManager.INVALID_APPWIDGET_ID
+ ) ?: AppWidgetManager.INVALID_APPWIDGET_ID
+
+ if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
+ Log.d(TAG, "INVALID_APPWIDGET_ID")
+ finish()
+ return
+ }
+
+ viewModel = ViewModelProvider(this)[NoteListViewModel::class.java]
+ adapterCategories = NavigationAdapter(this, this)
+
+ val views = ActivityInteractiveWidgetConfigurationBinding.inflate(layoutInflater)
+ binding = views
+ setContentView(views.root)
+ views.recyclerView.adapter = adapterCategories
+ views.saveWidget.setOnClickListener { saveWidget() }
+ restorePreferences(views)
+
+ loadAccount()
+ }
+
+ private fun restorePreferences(views: ActivityInteractiveWidgetConfigurationBinding) {
+ views.favoritesFirstCheckbox.isChecked =
+ InteractiveWidgetPreferences.isFavoritesFirst(this, appWidgetId)
+
+ val sortOrder = InteractiveWidgetPreferences.getSortOrder(this, appWidgetId)
+ if (sortOrder == WidgetSortOrder.OLDEST_FIRST) {
+ views.sortOldestFirst.isChecked = true
+ } else {
+ views.sortNewestFirst.isChecked = true
+ }
+ }
+
+ override fun onItemClick(item: NavigationItem) = select(item)
+
+ override fun onIconClick(item: NavigationItem) = select(item)
+
+ private fun applyNavigationItems(navigationItems: List) {
+ adapterCategories.setItems(navigationItems)
+
+ val item = navigationItems.firstOrNull { it.id == selectedItemId }
+ ?: navigationItems.firstOrNull { it.id == MainActivity.ADAPTER_KEY_RECENT }
+ ?: return
+
+ select(item)
+ }
+
+ private fun select(item: NavigationItem) {
+ selectedItem = item
+ selectedItemId = item.id
+ adapterCategories.setSelectedItem(item.id)
+ }
+
+ private fun saveWidget() {
+ val account = localAccount ?: run {
+ Log.w(TAG, "No account loaded; ignoring selection")
+ return
+ }
+ val item = selectedItem ?: run {
+ Log.w(TAG, "No category selected; ignoring selection")
+ return
+ }
+ val views = binding ?: return
+
+ val favoritesFirst = views.favoritesFirstCheckbox.isChecked
+ val sortOrder = if (views.sortOldestFirst.isChecked) {
+ WidgetSortOrder.OLDEST_FIRST
+ } else {
+ WidgetSortOrder.NEWEST_FIRST
+ }
+
+ val data = NotesListWidgetData().apply {
+ id = appWidgetId
+ accountId = account.id
+ themeMode = NotesApplication.getAppTheme(applicationContext).modeId
+ }
+ applyNavigationMode(data, item)
+
+ executor.execute {
+ repo.createOrUpdateNoteListWidgetData(data)
+ InteractiveWidgetPreferences.save(applicationContext, appWidgetId, favoritesFirst, sortOrder)
+
+ val updateIntent = Intent(
+ AppWidgetManager.ACTION_APPWIDGET_UPDATE,
+ null,
+ applicationContext,
+ InteractiveNoteListWidget::class.java
+ ).apply {
+ putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
+ putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, intArrayOf(appWidgetId))
+ }
+
+ applicationContext.sendBroadcast(updateIntent)
+
+ runOnUiThread {
+ setResult(Activity.RESULT_OK, updateIntent)
+ finish()
+ }
+ }
+ }
+
+ private fun applyNavigationMode(data: NotesListWidgetData, item: NavigationItem) {
+ when (item.type) {
+ ENavigationCategoryType.RECENT -> data.mode = NotesListWidgetData.MODE_DISPLAY_ALL
+ ENavigationCategoryType.FAVORITES -> data.mode = NotesListWidgetData.MODE_DISPLAY_STARRED
+ ENavigationCategoryType.UNCATEGORIZED -> {
+ data.mode = NotesListWidgetData.MODE_DISPLAY_CATEGORY
+ data.category = null
+ }
+
+ ENavigationCategoryType.DEFAULT_CATEGORY -> applyCategoryMode(data, item)
+ null -> fallBackToAllNotes(data)
+ }
+ }
+
+ private fun applyCategoryMode(data: NotesListWidgetData, item: NavigationItem) {
+ if (item !is NavigationItem.CategoryNavigationItem) {
+ fallBackToAllNotes(data)
+ return
+ }
+
+ data.mode = NotesListWidgetData.MODE_DISPLAY_CATEGORY
+ data.category = item.category
+ }
+
+ private fun fallBackToAllNotes(data: NotesListWidgetData) {
+ data.mode = NotesListWidgetData.MODE_DISPLAY_ALL
+ Log.e(TAG, "Unknown item navigation type. Fallback to show ${ENavigationCategoryType.RECENT}")
+ }
+
+ private fun loadAccount() {
+ executor.execute {
+ val account = currentAccount()
+ if (account == null) {
+ runOnUiThread {
+ Toast.makeText(this, R.string.widget_not_logged_in, Toast.LENGTH_LONG).show()
+ finish()
+ }
+ return@execute
+ }
+
+ localAccount = account
+ val storedData = repo.getNoteListWidgetData(appWidgetId)
+ selectedItemId = InteractiveWidgetSelection.navigationItemId(storedData)
+
+ runOnUiThread {
+ if (storedData != null) {
+ binding?.saveWidget?.setText(R.string.widget_edit)
+ }
+ viewModel.getAdapterCategories(account.id).observe(this, ::applyNavigationItems)
+ }
+ }
+ }
+
+ private fun currentAccount(): Account? = try {
+ repo.getAccountByName(SingleAccountHelper.getCurrentSingleSignOnAccount(this).name)
+ } catch (e: SSOException) {
+ Log.w(TAG, "Account not found", e)
+ null
+ }
+
+ override fun applyBrand(color: Int) {
+ val views = binding ?: return
+ val util = BrandingUtil.of(color, this)
+
+ util.platform.themeCheckbox(views.favoritesFirstCheckbox)
+ util.platform.themeRadioButton(views.sortNewestFirst)
+ util.platform.themeRadioButton(views.sortOldestFirst)
+ util.material.themeExtendedFAB(views.saveWidget)
+ }
+
+ companion object {
+ private val TAG: String = InteractiveNoteListWidgetConfigurationActivity::class.java.simpleName
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/widget/interactivelist/InteractiveNoteListWidgetFactory.kt b/app/src/main/java/it/niedermann/owncloud/notes/widget/interactivelist/InteractiveNoteListWidgetFactory.kt
new file mode 100644
index 000000000..502714cc2
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/widget/interactivelist/InteractiveNoteListWidgetFactory.kt
@@ -0,0 +1,182 @@
+/*
+ * Nextcloud Notes - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+package it.niedermann.owncloud.notes.widget.interactivelist
+
+import android.appwidget.AppWidgetManager
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.util.Log
+import android.view.View
+import android.widget.RemoteViews
+import android.widget.RemoteViewsService.RemoteViewsFactory
+import androidx.core.content.ContextCompat
+import androidx.core.net.toUri
+import com.nextcloud.android.common.ui.util.PlatformThemeUtil
+import it.niedermann.owncloud.notes.R
+import it.niedermann.owncloud.notes.edit.EditNoteActivity
+import it.niedermann.owncloud.notes.persistence.NotesRepository
+import it.niedermann.owncloud.notes.persistence.entity.Note
+import it.niedermann.owncloud.notes.persistence.entity.NotesListWidgetData
+import it.niedermann.owncloud.notes.shared.util.NoteUtil
+
+class InteractiveNoteListWidgetFactory internal constructor(private val context: Context, intent: Intent) :
+ RemoteViewsFactory {
+ private val appWidgetId: Int = intent.getIntExtra(
+ AppWidgetManager.EXTRA_APPWIDGET_ID,
+ AppWidgetManager.INVALID_APPWIDGET_ID
+ )
+ private val repo: NotesRepository = NotesRepository.getInstance(context)
+ private val dbNotes: MutableList = ArrayList()
+
+ override fun onCreate() = Unit
+
+ override fun onDataSetChanged() {
+ dbNotes.clear()
+ try {
+ val widgetData = repo.getNoteListWidgetData(appWidgetId)
+ if (widgetData == null) {
+ Log.w(TAG, "Widget data is null")
+ return
+ }
+
+ when (widgetData.mode) {
+ NotesListWidgetData.MODE_DISPLAY_ALL -> dbNotes.addAll(
+ repo.searchRecentByModified(
+ widgetData.accountId, "%"
+ )
+ )
+
+ NotesListWidgetData.MODE_DISPLAY_STARRED -> dbNotes.addAll(
+ repo.searchFavoritesByModified(
+ widgetData.accountId, "%"
+ )
+ )
+
+ else -> {
+ if (widgetData.category != null) {
+ dbNotes.addAll(
+ repo.searchCategoryByModified(
+ widgetData.accountId,
+ "%",
+ widgetData.category
+ )
+ )
+ } else {
+ dbNotes.addAll(
+ repo.searchUncategorizedByModified(
+ widgetData.accountId,
+ "%"
+ )
+ )
+ }
+ }
+ }
+
+ applySorting(context, appWidgetId, dbNotes)
+ } catch (e: Exception) {
+ Log.w(TAG, "Error caught at onDataSetChanged", e)
+ }
+ }
+
+ override fun onDestroy() = Unit
+
+ override fun getCount(): Int {
+ return dbNotes.size
+ }
+
+ override fun getViewAt(position: Int): RemoteViews? {
+ val note = dbNotes.getOrNull(position) ?: return null
+
+ return RemoteViews(context.packageName, R.layout.widget_interactive_entry).apply {
+ setOnClickFillInIntent(R.id.interactive_entry, getOpenNoteIntent(context, note))
+
+ setTextViewText(R.id.interactive_entry_title, note.title)
+
+ if (note.excerpt.isEmpty()) {
+ setViewVisibility(R.id.interactive_entry_excerpt, View.GONE)
+ } else {
+ setViewVisibility(R.id.interactive_entry_excerpt, View.VISIBLE)
+ setTextViewText(
+ R.id.interactive_entry_excerpt,
+ note.excerpt.replace(NoteUtil.EXCERPT_LINE_SEPARATOR, "\n")
+ )
+ }
+
+ if (note.category.isEmpty()) {
+ setViewVisibility(R.id.interactive_entry_category, View.GONE)
+ } else {
+ setViewVisibility(R.id.interactive_entry_category, View.VISIBLE)
+ setTextViewText(R.id.interactive_entry_category, note.category)
+
+ val textColorId = if (PlatformThemeUtil.isDarkMode(context)) {
+ R.color.text_color
+ } else {
+ R.color.category_border
+ }
+ val textColor = ContextCompat.getColor(context, textColorId)
+ setTextColor(R.id.interactive_entry_category, textColor)
+ }
+
+ if (note.favorite) {
+ setViewVisibility(R.id.interactive_entry_fav_icon, View.VISIBLE)
+ setImageViewResource(R.id.interactive_entry_fav_icon, R.drawable.ic_star_yellow_24dp)
+ } else {
+ setViewVisibility(R.id.interactive_entry_fav_icon, View.GONE)
+ }
+ }
+ }
+
+ override fun getLoadingView(): RemoteViews? {
+ return null
+ }
+
+ override fun getViewTypeCount(): Int {
+ return 1
+ }
+
+ override fun getItemId(position: Int): Long {
+ return dbNotes[position].id
+ }
+
+ override fun hasStableIds(): Boolean {
+ return true
+ }
+
+ companion object {
+ private val TAG: String = InteractiveNoteListWidgetFactory::class.java.simpleName
+ }
+}
+
+private fun applySorting(context: Context, appWidgetId: Int, notes: MutableList) {
+ val sortOrder = InteractiveWidgetPreferences.getSortOrder(context, appWidgetId)
+ val byDate: Comparator = if (sortOrder == WidgetSortOrder.OLDEST_FIRST) {
+ compareBy { it.modified?.timeInMillis ?: 0L }
+ } else {
+ compareByDescending { it.modified?.timeInMillis ?: 0L }
+ }
+
+ if (!InteractiveWidgetPreferences.isFavoritesFirst(context, appWidgetId)) {
+ notes.sortWith(byDate)
+ return
+ }
+
+ notes.sortWith(compareByDescending { it.favorite }.then(byDate))
+}
+
+private fun getOpenNoteIntent(context: Context, note: Note): Intent {
+ val bundle = Bundle().apply {
+ putLong(EditNoteActivity.PARAM_NOTE_ID, note.id)
+ putLong(EditNoteActivity.PARAM_ACCOUNT_ID, note.accountId)
+ }
+
+ return Intent(context, EditNoteActivity::class.java).apply {
+ setPackage(context.packageName)
+ putExtras(bundle)
+ data = toUri(Intent.URI_INTENT_SCHEME).toUri()
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/widget/interactivelist/InteractiveNoteListWidgetService.kt b/app/src/main/java/it/niedermann/owncloud/notes/widget/interactivelist/InteractiveNoteListWidgetService.kt
new file mode 100644
index 000000000..bc5bb7954
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/widget/interactivelist/InteractiveNoteListWidgetService.kt
@@ -0,0 +1,15 @@
+/*
+ * Nextcloud Notes - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+package it.niedermann.owncloud.notes.widget.interactivelist
+
+import android.content.Intent
+import android.widget.RemoteViewsService
+
+class InteractiveNoteListWidgetService : RemoteViewsService() {
+ override fun onGetViewFactory(intent: Intent): RemoteViewsFactory =
+ InteractiveNoteListWidgetFactory(applicationContext, intent)
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/widget/interactivelist/InteractiveWidgetPreferences.kt b/app/src/main/java/it/niedermann/owncloud/notes/widget/interactivelist/InteractiveWidgetPreferences.kt
new file mode 100644
index 000000000..12c245fea
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/widget/interactivelist/InteractiveWidgetPreferences.kt
@@ -0,0 +1,44 @@
+/*
+ * Nextcloud Notes - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+package it.niedermann.owncloud.notes.widget.interactivelist
+
+import android.content.Context
+import androidx.core.content.edit
+
+object InteractiveWidgetPreferences {
+ private const val PREFS_NAME = "interactive_note_list_widget"
+ private const val KEY_FAVORITES_FIRST = "favorites_first_"
+ private const val KEY_SORT_ORDER = "sort_order_"
+
+ private const val DEFAULT_FAVORITES_FIRST = true
+
+ fun save(context: Context, appWidgetId: Int, favoritesFirst: Boolean, sortOrder: WidgetSortOrder) {
+ prefs(context).edit {
+ putBoolean(KEY_FAVORITES_FIRST + appWidgetId, favoritesFirst)
+ putString(KEY_SORT_ORDER + appWidgetId, sortOrder.name)
+ }
+ }
+
+ fun isFavoritesFirst(context: Context, appWidgetId: Int): Boolean =
+ prefs(context).getBoolean(KEY_FAVORITES_FIRST + appWidgetId, DEFAULT_FAVORITES_FIRST)
+
+ fun getSortOrder(context: Context, appWidgetId: Int): WidgetSortOrder {
+ val stored = prefs(context).getString(KEY_SORT_ORDER + appWidgetId, null)
+ ?: return WidgetSortOrder.NEWEST_FIRST
+ return runCatching { WidgetSortOrder.valueOf(stored) }.getOrDefault(WidgetSortOrder.NEWEST_FIRST)
+ }
+
+ fun remove(context: Context, appWidgetId: Int) {
+ prefs(context).edit {
+ remove(KEY_FAVORITES_FIRST + appWidgetId)
+ remove(KEY_SORT_ORDER + appWidgetId)
+ }
+ }
+
+ private fun prefs(context: Context) =
+ context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/widget/interactivelist/InteractiveWidgetSelection.kt b/app/src/main/java/it/niedermann/owncloud/notes/widget/interactivelist/InteractiveWidgetSelection.kt
new file mode 100644
index 000000000..88699aa0c
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/widget/interactivelist/InteractiveWidgetSelection.kt
@@ -0,0 +1,35 @@
+/*
+ * Nextcloud Notes - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+package it.niedermann.owncloud.notes.widget.interactivelist
+
+import it.niedermann.owncloud.notes.main.MainActivity
+import it.niedermann.owncloud.notes.persistence.entity.NotesListWidgetData
+
+object InteractiveWidgetSelection {
+ private const val CATEGORY_ID_PREFIX = "category:"
+ private const val CATEGORY_SEPARATOR = '/'
+
+ fun navigationItemId(data: NotesListWidgetData?): String {
+ if (data == null) {
+ return MainActivity.ADAPTER_KEY_RECENT
+ }
+
+ return when (data.mode) {
+ NotesListWidgetData.MODE_DISPLAY_STARRED -> MainActivity.ADAPTER_KEY_STARRED
+ NotesListWidgetData.MODE_DISPLAY_CATEGORY -> categoryId(data.category)
+ else -> MainActivity.ADAPTER_KEY_RECENT
+ }
+ }
+
+ private fun categoryId(category: String?): String {
+ if (category.isNullOrEmpty()) {
+ return MainActivity.ADAPTER_KEY_UNCATEGORIZED
+ }
+
+ return CATEGORY_ID_PREFIX + category.substringBefore(CATEGORY_SEPARATOR)
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/widget/interactivelist/WidgetSortOrder.kt b/app/src/main/java/it/niedermann/owncloud/notes/widget/interactivelist/WidgetSortOrder.kt
new file mode 100644
index 000000000..645b449e7
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/widget/interactivelist/WidgetSortOrder.kt
@@ -0,0 +1,12 @@
+/*
+ * Nextcloud Notes - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+package it.niedermann.owncloud.notes.widget.interactivelist
+
+enum class WidgetSortOrder {
+ NEWEST_FIRST,
+ OLDEST_FIRST
+}
diff --git a/app/src/main/res/drawable/widget_card_background.xml b/app/src/main/res/drawable/widget_card_background.xml
new file mode 100644
index 000000000..44654b997
--- /dev/null
+++ b/app/src/main/res/drawable/widget_card_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/widget_fab_background.xml b/app/src/main/res/drawable/widget_fab_background.xml
new file mode 100644
index 000000000..321ed54d6
--- /dev/null
+++ b/app/src/main/res/drawable/widget_fab_background.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/widget_interactive_background.xml b/app/src/main/res/drawable/widget_interactive_background.xml
new file mode 100644
index 000000000..aa3c3d2d4
--- /dev/null
+++ b/app/src/main/res/drawable/widget_interactive_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_interactive_widget_configuration.xml b/app/src/main/res/layout/activity_interactive_widget_configuration.xml
new file mode 100644
index 000000000..fed71a812
--- /dev/null
+++ b/app/src/main/res/layout/activity_interactive_widget_configuration.xml
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/widget_interactive_entry.xml b/app/src/main/res/layout/widget_interactive_entry.xml
new file mode 100644
index 000000000..a605c6d31
--- /dev/null
+++ b/app/src/main/res/layout/widget_interactive_entry.xml
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/widget_interactive_note_list.xml b/app/src/main/res/layout/widget_interactive_note_list.xml
new file mode 100644
index 000000000..c72bfdb58
--- /dev/null
+++ b/app/src/main/res/layout/widget_interactive_note_list.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/widget_interactive_preview.xml b/app/src/main/res/layout/widget_interactive_preview.xml
new file mode 100644
index 000000000..afd8ed8d6
--- /dev/null
+++ b/app/src/main/res/layout/widget_interactive_preview.xml
@@ -0,0 +1,128 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml
index 0857691c8..e681da78d 100644
--- a/app/src/main/res/values-night/colors.xml
+++ b/app/src/main/res/values-night/colors.xml
@@ -35,6 +35,7 @@
#dd000000
#d8d8d8
+ #2b2b2b
#222222
#ffffff
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index f3a39be8a..a72fd5899 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -47,6 +47,7 @@
#ddffffff
#222222
+ #ffffff
#ededed
#666666
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index b42e9c41b..cf8f26444 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -76,6 +76,10 @@
4dp
26dp
20dp
+ 48dp
+ @dimen/spacer_2x
+ 11dp
+ 88dp
48dp
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 80d4284fc..509154536 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -245,6 +245,16 @@
Note not found
Please login to Notes before using this widget
Star icon is used to denote an item as a favorite
+ Interactive notes
+ Create note
+ Favorites
+ Favorites on top
+ Sort order
+ Newest first
+ Oldest first
+ Notes to show
+ Add widget
+ Edit widget
Select note
diff --git a/app/src/main/res/xml/interactive_note_list_widget_provider_info.xml b/app/src/main/res/xml/interactive_note_list_widget_provider_info.xml
new file mode 100644
index 000000000..fa5d1decb
--- /dev/null
+++ b/app/src/main/res/xml/interactive_note_list_widget_provider_info.xml
@@ -0,0 +1,21 @@
+
+
+
diff --git a/app/src/test/java/it/niedermann/owncloud/notes/persistence/WidgetNotesListDaoTest.kt b/app/src/test/java/it/niedermann/owncloud/notes/persistence/WidgetNotesListDaoTest.kt
new file mode 100644
index 000000000..7e3ff6806
--- /dev/null
+++ b/app/src/test/java/it/niedermann/owncloud/notes/persistence/WidgetNotesListDaoTest.kt
@@ -0,0 +1,93 @@
+/*
+ * Nextcloud Notes - Android Client
+ *
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+package it.niedermann.owncloud.notes.persistence
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import androidx.room.Room
+import androidx.test.core.app.ApplicationProvider
+import it.niedermann.owncloud.notes.persistence.entity.Account
+import it.niedermann.owncloud.notes.persistence.entity.NotesListWidgetData
+import it.niedermann.owncloud.notes.shared.model.Capabilities
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+
+@RunWith(RobolectricTestRunner::class)
+@Config(sdk = [36])
+class WidgetNotesListDaoTest {
+
+ @get:Rule
+ val instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ private lateinit var db: NotesDatabase
+ private var accountId: Long = 0
+
+ @Before
+ fun setupDB() {
+ db = Room
+ .inMemoryDatabaseBuilder(ApplicationProvider.getApplicationContext(), NotesDatabase::class.java)
+ .allowMainThreadQueries()
+ .build()
+
+ val account = Account(ACCOUNT_URL, ACCOUNT_USERNAME, ACCOUNT_NAME, null, Capabilities())
+ accountId = db.accountDao.insert(account)
+ }
+
+ @After
+ fun closeDb() {
+ db.close()
+ }
+
+ @Test
+ fun insertThenReadWidgetData() {
+ db.widgetNotesListDao.createOrUpdateNoteListWidgetData(
+ widgetData(NotesListWidgetData.MODE_DISPLAY_ALL)
+ )
+
+ val loaded = db.widgetNotesListDao.getNoteListWidgetData(WIDGET_ID)
+ assertNotNull(loaded)
+ assertEquals(NotesListWidgetData.MODE_DISPLAY_ALL, loaded.mode)
+ }
+
+ /**
+ * Reconfiguring an interactive widget saves again for the same appWidgetId (primary key).
+ * The DAO must upsert; a plain insert would abort on the primary-key conflict.
+ */
+ @Test
+ fun reconfigureUpdatesExistingWidgetData() {
+ db.widgetNotesListDao.createOrUpdateNoteListWidgetData(
+ widgetData(NotesListWidgetData.MODE_DISPLAY_ALL)
+ )
+ db.widgetNotesListDao.createOrUpdateNoteListWidgetData(
+ widgetData(NotesListWidgetData.MODE_DISPLAY_STARRED)
+ )
+
+ val loaded = db.widgetNotesListDao.getNoteListWidgetData(WIDGET_ID)
+ assertNotNull(loaded)
+ assertEquals(NotesListWidgetData.MODE_DISPLAY_STARRED, loaded.mode)
+ }
+
+ private fun widgetData(mode: Int) = NotesListWidgetData().apply {
+ id = WIDGET_ID
+ accountId = this@WidgetNotesListDaoTest.accountId
+ this.mode = mode
+ category = null
+ }
+
+ companion object {
+ private const val WIDGET_ID = 1
+ private const val ACCOUNT_URL = "https://example.com"
+ private const val ACCOUNT_USERNAME = "user"
+ private const val ACCOUNT_NAME = "user@example.com"
+ }
+}
diff --git a/app/src/test/java/it/niedermann/owncloud/notes/shared/util/NoteUtilTest.kt b/app/src/test/java/it/niedermann/owncloud/notes/shared/util/NoteUtilTest.kt
index ed2495f92..c99482154 100644
--- a/app/src/test/java/it/niedermann/owncloud/notes/shared/util/NoteUtilTest.kt
+++ b/app/src/test/java/it/niedermann/owncloud/notes/shared/util/NoteUtilTest.kt
@@ -64,6 +64,7 @@ class NoteUtilTest : TestCase() {
fun testGenerateNoteExcerpt() {
testBasicExcerpts()
testTitleMatchingExcerpts()
+ testTitleAsPrefixOfFirstLine()
testEmptyLineHandling()
testMarkdownInContent()
testMarkdownInTitle()
@@ -89,6 +90,12 @@ class NoteUtilTest : TestCase() {
assertEquals("", NoteUtil.generateNoteExcerpt("", "Title"))
}
+ private fun testTitleAsPrefixOfFirstLine() {
+ assertEquals("Asdasd1 Asda22", NoteUtil.generateNoteExcerpt("Asdasd1\nAsda22", "Asdasd"))
+ assertEquals("Titles Foo", NoteUtil.generateNoteExcerpt("Titles\nFoo", "Title"))
+ assertEquals("Title suffix", NoteUtil.generateNoteExcerpt("Title suffix", "Title"))
+ }
+
private fun testEmptyLineHandling() {
// some empty lines between the actual contents ā Should be ignored
assertEquals("", NoteUtil.generateNoteExcerpt("\nTitle", "Title"))