diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 111a0dc01..89d2b95d2 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -122,6 +122,16 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/NotesApplication.java b/app/src/main/java/it/niedermann/owncloud/notes/NotesApplication.java
index e82e27bb9..b68c2f0b3 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/NotesApplication.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/NotesApplication.java
@@ -35,7 +35,7 @@ public class NotesApplication extends Application {
private static boolean isLocked = true;
private static long lastInteraction = 0;
private static String PREF_KEY_THEME;
- private static boolean isGridViewEnabled = false;
+ private static boolean isGridViewEnabled = true;
private static boolean isSwipeEnabled = true;
private static BrandingUtil brandingUtil;
@@ -45,7 +45,7 @@ public void onCreate() {
setAppTheme(getAppTheme(getApplicationContext()));
final var prefs = getDefaultSharedPreferences(getApplicationContext());
lockedPreference = prefs.getBoolean(getString(R.string.pref_key_lock), false);
- isGridViewEnabled = getDefaultSharedPreferences(this).getBoolean(getString(R.string.pref_key_gridview), false);
+ isGridViewEnabled = getDefaultSharedPreferences(this).getBoolean(getString(R.string.pref_key_gridview), true);
isSwipeEnabled = getDefaultSharedPreferences(this).getBoolean(getString(R.string.pref_key_swipe_actions), true);
super.onCreate();
brandingUtil = BrandingUtil.getInstance(this);
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java
index f64eec2e7..55e67e926 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/BaseNoteFragment.java
@@ -9,6 +9,7 @@
import static it.niedermann.owncloud.notes.edit.EditNoteActivity.ACTION_SHORTCUT;
import static it.niedermann.owncloud.notes.shared.util.WidgetUtil.pendingIntentFlagCompat;
+import android.app.Activity;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
@@ -21,6 +22,11 @@
import android.view.View;
import android.widget.ScrollView;
+import android.net.Uri;
+import android.widget.Toast;
+
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -53,6 +59,7 @@
import it.niedermann.owncloud.notes.shared.model.DBStatus;
import it.niedermann.owncloud.notes.shared.model.ISyncCallback;
import it.niedermann.owncloud.notes.shared.util.ApiVersionUtil;
+import it.niedermann.owncloud.notes.shared.util.NoteImageHelper;
import it.niedermann.owncloud.notes.shared.util.NoteUtil;
import it.niedermann.owncloud.notes.shared.util.ShareUtil;
@@ -83,6 +90,8 @@ public abstract class BaseNoteFragment extends BrandedFragment implements Catego
protected boolean isNew = true;
+ private ActivityResultLauncher pickImageLauncher;
+
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
@@ -94,6 +103,12 @@ public void onAttach(@NonNull Context context) {
repo = NotesRepository.getInstance(context);
}
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ pickImageLauncher = registerForActivityResult(new ActivityResultContracts.GetContent(), this::onImagePicked);
+ }
+
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
@@ -239,6 +254,12 @@ public void onPrepareOptionsMenu(@NonNull Menu menu) {
if (note != null) {
prepareFavoriteOption(menu.findItem(R.id.menu_favorite));
+ final var attachItem = menu.findItem(R.id.menu_attach_image);
+ if (attachItem != null) {
+ final var utils = BrandingUtil.of(colorAccent, requireContext());
+ utils.platform.colorToolbarMenuIcon(requireContext(), attachItem);
+ }
+
final var preferredApiVersion = ApiVersionUtil.getPreferredApiVersion(localAccount.getApiVersion());
menu.findItem(R.id.menu_title).setVisible(preferredApiVersion != null && preferredApiVersion.compareTo(ApiVersion.API_VERSION_1_0) >= 0);
menu.findItem(R.id.menu_delete).setVisible(!isNew);
@@ -264,7 +285,12 @@ private void prepareFavoriteOption(MenuItem item) {
@Override
public boolean onOptionsItemSelected(MenuItem item) {
final int itemId = item.getItemId();
- if (itemId == R.id.menu_cancel) {
+ if (itemId == R.id.menu_attach_image) {
+ if (pickImageLauncher != null) {
+ pickImageLauncher.launch("image/*");
+ }
+ return true;
+ } else if (itemId == R.id.menu_cancel) {
executor.submit(() -> {
if (originalNote == null) {
repo.deleteNoteAndSync(localAccount, note.getId());
@@ -315,6 +341,63 @@ public boolean onOptionsItemSelected(MenuItem item) {
return super.onOptionsItemSelected(item);
}
+ private void onImagePicked(@Nullable Uri uri) {
+ if (uri == null) {
+ return;
+ }
+ final Context context = getContext();
+ if (context == null) {
+ return;
+ }
+ Toast.makeText(context, R.string.uploading_image, Toast.LENGTH_SHORT).show();
+ executor.submit(() -> {
+ try {
+ final var ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(requireContext().getApplicationContext());
+ final String attachmentPath = NoteImageHelper.uploadImage(requireContext().getApplicationContext(), ssoAccount, uri);
+ final String markdown = NoteImageHelper.formatMarkdownImage(attachmentPath);
+ final Activity activity = getActivity();
+ if (activity != null && isAdded()) {
+ activity.runOnUiThread(() -> {
+ if (isAdded() && getContext() != null) {
+ insertImageMarkdown(markdown);
+ Toast.makeText(getContext(), R.string.image_attached, Toast.LENGTH_SHORT).show();
+ }
+ });
+ }
+ } catch (Exception e) {
+ Log_OC.e(TAG, "Failed to upload image attachment", e);
+ final Activity activity = getActivity();
+ if (activity != null && isAdded()) {
+ activity.runOnUiThread(() -> {
+ if (getContext() != null) {
+ Toast.makeText(getContext(), "Failed to attach image: " + e.getLocalizedMessage(), Toast.LENGTH_LONG).show();
+ }
+ });
+ }
+ }
+ });
+ }
+
+ public void insertImageMarkdown(@NonNull String markdown) {
+ if (this instanceof NoteEditFragment) {
+ ((NoteEditFragment) this).insertTextAtCursor(markdown);
+ saveNote(null);
+ } else {
+ if (note != null) {
+ String current = note.getContent();
+ String updated = (current == null || current.isEmpty()) ? markdown : (current + markdown);
+ note = repo.updateNoteAndSync(localAccount, note, updated, null, null);
+ if (listener != null) {
+ listener.onNoteUpdated(note);
+ }
+ requireActivity().invalidateOptionsMenu();
+ if (this instanceof NotePreviewFragment) {
+ ((NotePreviewFragment) this).onNoteLoaded(note);
+ }
+ }
+ }
+ }
+
private void pinNoteToHome() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return;
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/EditNoteActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/EditNoteActivity.java
index e94d30973..ba6cabea0 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/edit/EditNoteActivity.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/EditNoteActivity.java
@@ -12,15 +12,18 @@
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
+import android.net.Uri;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.WindowManager;
+import android.widget.ProgressBar;
import android.widget.Toast;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowCompat;
@@ -28,6 +31,8 @@
import androidx.fragment.app.Fragment;
import androidx.preference.PreferenceManager;
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+
import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
import com.nextcloud.android.sso.exceptions.NoCurrentAccountSelectedException;
import com.nextcloud.android.sso.helper.SingleAccountHelper;
@@ -36,7 +41,9 @@
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
+import java.util.ArrayList;
import java.util.Calendar;
+import java.util.List;
import java.util.Objects;
import it.niedermann.android.sharedpreferences.SharedPreferenceBooleanLiveData;
@@ -50,6 +57,7 @@
import it.niedermann.owncloud.notes.persistence.entity.Account;
import it.niedermann.owncloud.notes.persistence.entity.Note;
import it.niedermann.owncloud.notes.shared.model.NavigationCategory;
+import it.niedermann.owncloud.notes.shared.util.NoteImageHelper;
import it.niedermann.owncloud.notes.shared.util.NoteUtil;
import it.niedermann.owncloud.notes.shared.util.ShareUtil;
@@ -343,6 +351,16 @@ private void launchNewNote() {
favorite = categoryPreselection.getType() == FAVORITES;
}
+ final String action = intent.getAction();
+ final String type = intent.getType();
+ final boolean isImageShare = (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action))
+ && ((type != null && type.startsWith("image/")) || intent.hasExtra(Intent.EXTRA_STREAM));
+
+ if (isImageShare) {
+ handleImageShare(intent, categoryTitle, favorite);
+ return;
+ }
+
String content = "";
if (
intent.hasExtra(Intent.EXTRA_TEXT) &&
@@ -363,6 +381,83 @@ private void launchNewNote() {
replaceFragment();
}
+ private void handleImageShare(Intent intent, String categoryTitle, boolean favorite) {
+ final List imageUris = new ArrayList<>();
+ if (Intent.ACTION_SEND_MULTIPLE.equals(intent.getAction())) {
+ List uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
+ if (uris != null) {
+ imageUris.addAll(uris);
+ }
+ } else {
+ Uri uri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
+ if (uri != null) {
+ imageUris.add(uri);
+ } else if (intent.getData() != null) {
+ imageUris.add(intent.getData());
+ }
+ }
+
+ final String initialText = intent.hasExtra(Intent.EXTRA_TEXT) ? ShareUtil.extractSharedText(intent) : "";
+
+ if (imageUris.isEmpty()) {
+ final String content = initialText != null ? initialText : "";
+ final var newNote = new Note(null, Calendar.getInstance(), NoteUtil.generateNonEmptyNoteTitle(content, this), content, categoryTitle, favorite, null, false, false);
+ fragment = getNewNoteFragment(newNote);
+ replaceFragment();
+ return;
+ }
+
+ final ProgressBar progressBar = new ProgressBar(this);
+ progressBar.setPadding(0, 48, 0, 48);
+ final AlertDialog progressDialog = new MaterialAlertDialogBuilder(this)
+ .setTitle(R.string.uploading_image)
+ .setView(progressBar)
+ .setCancelable(false)
+ .show();
+
+ new Thread(() -> {
+ StringBuilder contentBuilder = new StringBuilder();
+ if (initialText != null && !initialText.trim().isEmpty()) {
+ contentBuilder.append(initialText).append("\n\n");
+ }
+
+ try {
+ final SingleSignOnAccount ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(getApplicationContext());
+ final String notesPath = NoteImageHelper.getNotesPath(getApplicationContext(), ssoAccount, repo);
+
+ for (Uri uri : imageUris) {
+ try {
+ String attachmentPath = NoteImageHelper.uploadImage(getApplicationContext(), ssoAccount, notesPath, uri);
+ contentBuilder.append(NoteImageHelper.formatMarkdownImage(attachmentPath));
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to upload shared image " + uri, e);
+ }
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to get SSO account or notes path for image share", e);
+ }
+
+ final String finalContent = contentBuilder.toString().trim();
+ runOnUiThread(() -> {
+ if (isFinishing() || isDestroyed()) {
+ return;
+ }
+ try {
+ if (progressDialog.isShowing()) {
+ progressDialog.dismiss();
+ }
+ } catch (Exception ignored) {
+ }
+
+ String title = NoteUtil.generateNonEmptyNoteTitle(finalContent, EditNoteActivity.this);
+ final var newNote = new Note(null, Calendar.getInstance(), title, finalContent, categoryTitle, favorite, null, false, false);
+ fragment = getNewNoteFragment(newNote);
+ replaceFragment();
+ Toast.makeText(EditNoteActivity.this, R.string.image_attached, Toast.LENGTH_SHORT).show();
+ });
+ }).start();
+ }
+
private void launchReadonlyNote() {
final var intent = getIntent();
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/NoteEditFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/NoteEditFragment.java
index f0a8c334d..1e4016dd5 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/edit/NoteEditFragment.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/NoteEditFragment.java
@@ -223,6 +223,20 @@ protected void onNoteLoaded(Note note) {
});
}
+ public void insertTextAtCursor(@NonNull String text) {
+ if (binding == null || binding.editContent == null) {
+ return;
+ }
+ int start = Math.max(0, binding.editContent.getSelectionStart());
+ int end = Math.max(0, binding.editContent.getSelectionEnd());
+ int selStart = Math.min(start, end);
+ int selEnd = Math.max(start, end);
+ if (binding.editContent.getText() != null) {
+ binding.editContent.getText().replace(selStart, selEnd, text, 0, text.length());
+ binding.editContent.setSelection(selStart + text.length());
+ }
+ }
+
private void openSoftKeyboard() {
binding.editContent.postDelayed(() -> {
binding.editContent.requestFocus();
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/NotePreviewFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/NotePreviewFragment.java
index e7bbde4d2..35ddc8ddc 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/edit/NotePreviewFragment.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/NotePreviewFragment.java
@@ -32,12 +32,14 @@
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.nextcloud.android.sso.helper.SingleAccountHelper;
+import com.nextcloud.android.sso.model.SingleSignOnAccount;
import com.owncloud.android.lib.common.utils.Log_OC;
import it.niedermann.owncloud.notes.R;
import it.niedermann.owncloud.notes.branding.BrandingUtil;
import it.niedermann.owncloud.notes.databinding.FragmentNotePreviewBinding;
import it.niedermann.owncloud.notes.persistence.entity.Note;
+import it.niedermann.owncloud.notes.shared.model.ApiVersion;
import it.niedermann.owncloud.notes.shared.util.SSOUtil;
import kotlin.Unit;
@@ -153,7 +155,36 @@ protected void onNoteLoaded(Note note) {
final String content = note.getContent();
changedText = content;
+ // Resolve the notes folder path from the server (e.g. "Notes") to build
+ // the correct WebDAV prefix for relative image references like
+ // "attachments/image.png" → "http://server/remote.php/webdav/Notes/attachments/image.png"
+ String imageUrlPrefix = "";
+ try {
+ final SingleSignOnAccount ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(requireContext());
+ final var settingsCall = repo.getServerSettings(ssoAccount, ApiVersion.API_VERSION_1_0);
+ final var settingsResponse = settingsCall.execute();
+ final var settings = settingsResponse.body();
+ final String notesPath = (settings != null && settings.getNotesPath() != null)
+ ? settings.getNotesPath()
+ : "Notes";
+ // Build WebDAV prefix: serverUrl + /remote.php/webdav/ + notesPath + /
+ imageUrlPrefix = ssoAccount.url + "/remote.php/webdav/" + notesPath + "/";
+ Log.i(TAG, "Image URL prefix set to: " + imageUrlPrefix);
+ } catch (Exception e) {
+ Log_OC.w(TAG, "Could not fetch notes path for image prefix, using server root: " + e.getMessage());
+ try {
+ final SingleSignOnAccount ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(requireContext());
+ imageUrlPrefix = ssoAccount.url + "/remote.php/webdav/Notes/";
+ } catch (Exception e2) {
+ Log_OC.e(TAG, "Failed to get ssoAccount for image prefix: " + e2);
+ }
+ }
+
+ final String finalImageUrlPrefix = imageUrlPrefix;
onMainThread(() -> {
+ if (!finalImageUrlPrefix.isEmpty()) {
+ binding.singleNoteContent.setMarkdownImageUrlPrefix(finalImageUrlPrefix);
+ }
binding.singleNoteContent.setMarkdownString(content, setScrollY);
final var activity = getActivity();
@@ -246,6 +277,10 @@ public void applyBrand(int color) {
try {
final var ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(getContext());
binding.singleNoteContent.setCurrentSingleSignOnAccount(ssoAccount, color);
+ // Enable inline image display: provide the server base URL so that relative
+ // image paths in Markdown (e.g. "./Photos/img.jpg") are resolved correctly,
+ // and absolute Nextcloud paths (e.g. "/remote.php/webdav/...") work too.
+ binding.singleNoteContent.setMarkdownImageUrlPrefix(ssoAccount.url);
} catch (Exception e) {
Log_OC.e(TAG, "applyBrand exception: " + e);
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/edit/NoteReadonlyFragment.java b/app/src/main/java/it/niedermann/owncloud/notes/edit/NoteReadonlyFragment.java
index 7a1126f59..0562599b2 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/edit/NoteReadonlyFragment.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/edit/NoteReadonlyFragment.java
@@ -24,6 +24,9 @@ public class NoteReadonlyFragment extends NotePreviewFragment {
public void onPrepareOptionsMenu(@NonNull Menu menu) {
super.onPrepareOptionsMenu(menu);
menu.findItem(R.id.menu_favorite).setVisible(false);
+ if (menu.findItem(R.id.menu_attach_image) != null) {
+ menu.findItem(R.id.menu_attach_image).setVisible(false);
+ }
menu.findItem(R.id.menu_edit).setVisible(false);
menu.findItem(R.id.menu_preview).setVisible(false);
menu.findItem(R.id.menu_cancel).setVisible(false);
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolder.java b/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolder.java
index 67c5c53d9..da945a2e5 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolder.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolder.java
@@ -25,6 +25,7 @@
import it.niedermann.owncloud.notes.main.items.NoteViewHolder;
import it.niedermann.owncloud.notes.persistence.entity.Note;
import it.niedermann.owncloud.notes.shared.model.NoteClickListener;
+import it.niedermann.owncloud.notes.shared.util.NoteImagePreviewLoader;
public class NoteViewGridHolder extends NoteViewHolder {
@NonNull
@@ -54,9 +55,12 @@ public void bind(boolean isSelected, @NonNull Note note, boolean showCategory, @
bindFavorite(binding.noteFavorite, note.getFavorite());
bindModified(binding.noteModified, note.getModified());
bindSearchableContent(context, binding.noteTitle, searchQuery, note.getTitle(), color);
- bindSearchableContent(context, binding.noteExcerpt, searchQuery, note.getExcerpt().replace(EXCERPT_LINE_SEPARATOR, "\n"), color);
+ final boolean hasImagePreview = NoteImagePreviewLoader.load(context, note.getContent(), binding.noteImagePreview);
+ if (!hasImagePreview) {
+ bindSearchableContent(context, binding.noteExcerpt, searchQuery, note.getExcerpt().replace(EXCERPT_LINE_SEPARATOR, "\n"), color);
+ }
bindNoteSharedIcon(context, note.isShared(), binding.noteShared, color);
- binding.noteExcerpt.setVisibility(TextUtils.isEmpty(note.getExcerpt()) ? GONE : VISIBLE);
+ binding.noteExcerpt.setVisibility(hasImagePreview || TextUtils.isEmpty(note.getExcerpt()) ? GONE : VISIBLE);
}
@Nullable
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolderOnlyTitle.java b/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolderOnlyTitle.java
index 0699dd237..08a199c03 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolderOnlyTitle.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/grid/NoteViewGridHolderOnlyTitle.java
@@ -19,6 +19,7 @@
import it.niedermann.owncloud.notes.main.items.NoteViewHolder;
import it.niedermann.owncloud.notes.persistence.entity.Note;
import it.niedermann.owncloud.notes.shared.model.NoteClickListener;
+import it.niedermann.owncloud.notes.shared.util.NoteImagePreviewLoader;
public class NoteViewGridHolderOnlyTitle extends NoteViewHolder {
@NonNull
@@ -46,6 +47,7 @@ public void bind(boolean isSelected, @NonNull Note note, boolean showCategory, i
bindFavorite(binding.noteFavorite, note.getFavorite());
bindModified(binding.noteModified, note.getModified());
bindSearchableContent(context, binding.noteTitle, searchQuery, note.getTitle(), color);
+ NoteImagePreviewLoader.load(context, note.getContent(), binding.noteImagePreview);
bindNoteSharedIcon(context, note.isShared(), binding.noteShared, color);
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NoteViewListHolder.java b/app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NoteViewListHolder.java
index 771db1737..69cc6a9cc 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NoteViewListHolder.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/items/list/NoteViewListHolder.java
@@ -20,6 +20,7 @@
import it.niedermann.owncloud.notes.persistence.entity.Note;
import it.niedermann.owncloud.notes.shared.model.DBStatus;
import it.niedermann.owncloud.notes.shared.model.NoteClickListener;
+import it.niedermann.owncloud.notes.shared.util.NoteImagePreviewLoader;
public class NoteViewListHolder extends NoteViewHolder {
@NonNull
@@ -62,7 +63,10 @@ public void bind(boolean isSelected, @NonNull Note note, boolean showCategory, @
bindModified(binding.noteModified, note.getModified());
bindSearchableContent(context, binding.noteTitle, searchQuery, note.getTitle(), color);
- if (note.getExcerpt().isEmpty()) {
+ final boolean hasImagePreview = NoteImagePreviewLoader.load(context, note.getContent(), binding.noteImagePreview);
+ if (hasImagePreview) {
+ binding.noteExcerpt.setVisibility(View.GONE);
+ } else if (note.getExcerpt().isEmpty()) {
bindSearchableContent(
context,
binding.noteExcerpt,
@@ -70,8 +74,10 @@ public void bind(boolean isSelected, @NonNull Note note, boolean showCategory, @
context.getString(R.string.listview_no_content),
color
);
+ binding.noteExcerpt.setVisibility(View.VISIBLE);
} else {
bindSearchableContent(context, binding.noteExcerpt, searchQuery, note.getExcerpt(), color);
+ binding.noteExcerpt.setVisibility(View.VISIBLE);
}
bindNoteSharedIcon(context, note.isShared(), binding.noteShared, color);
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java
index 8143a9a9e..0db1f2a75 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java
@@ -121,7 +121,7 @@ public synchronized ShareAPI getShareAPI(@NonNull Context context, @NonNull Sing
return shareAPI;
}
- private synchronized NextcloudAPI getNextcloudAPI(@NonNull Context context, @NonNull SingleSignOnAccount ssoAccount) {
+ public synchronized NextcloudAPI getNextcloudAPI(@NonNull Context context, @NonNull SingleSignOnAccount ssoAccount) {
if (API_CACHE.containsKey(ssoAccount.name)) {
return API_CACHE.get(ssoAccount.name);
} else {
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/NoteDao.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/NoteDao.java
index ad539dc1f..a4373936c 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/NoteDao.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/NoteDao.java
@@ -24,7 +24,8 @@
/**
* Each method starting with search will return only a partial {@link Note} without any
- * {@link Note#eTag}, {@link Note#status}, {@link Note#content} or {@link Note#scrollY} for performance reasons.
+ * {@link Note#eTag} or {@link Note#scrollY}. The content contains only the bounded attachment-path fragment
+ * needed for image previews.
*/
@SuppressWarnings("JavadocReference")
@Dao
@@ -39,14 +40,15 @@ public interface NoteDao {
String getNoteById = "SELECT * FROM NOTE WHERE id = :id";
String count = "SELECT COUNT(*) FROM NOTE WHERE status != 'LOCAL_DELETED' AND accountId = :accountId";
String countFavorites = "SELECT COUNT(*) FROM NOTE WHERE status != 'LOCAL_DELETED' AND accountId = :accountId AND favorite = 1";
- String searchRecentByModified = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, '' as content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) ORDER BY favorite DESC, modified DESC";
- String searchRecentLexicographically = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, '' as content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) ORDER BY favorite DESC, title COLLATE LOCALIZED ASC";
- String searchFavoritesByModified = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, '' as content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) AND favorite = 1 ORDER BY modified DESC";
- String searchFavoritesLexicographically = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, '' as content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) AND favorite = 1 ORDER BY title COLLATE LOCALIZED ASC";
- String searchUncategorizedByModified = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, '' as content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) AND category = '' ORDER BY favorite DESC, modified DESC";
- String searchUncategorizedLexicographically = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, '' as content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) AND category = '' ORDER BY favorite DESC, title COLLATE LOCALIZED ASC";
- String searchCategoryByModified = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, '' as content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) AND (category = :category OR category LIKE :category || '/%') ORDER BY category, favorite DESC, modified DESC";
- String searchCategoryLexicographically = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, '' as content, 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) AND (category = :category OR category LIKE :category || '/%') ORDER BY category, favorite DESC, title COLLATE LOCALIZED ASC";
+ String imagePreviewPath = "CASE WHEN instr(content, 'attachments/') > 0 THEN substr(content, instr(content, 'attachments/'), 512) ELSE '' END as content";
+ String searchRecentByModified = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, " + imagePreviewPath + ", 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) ORDER BY favorite DESC, modified DESC";
+ String searchRecentLexicographically = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, " + imagePreviewPath + ", 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) ORDER BY favorite DESC, title COLLATE LOCALIZED ASC";
+ String searchFavoritesByModified = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, " + imagePreviewPath + ", 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) AND favorite = 1 ORDER BY modified DESC";
+ String searchFavoritesLexicographically = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, " + imagePreviewPath + ", 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) AND favorite = 1 ORDER BY title COLLATE LOCALIZED ASC";
+ String searchUncategorizedByModified = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, " + imagePreviewPath + ", 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) AND category = '' ORDER BY favorite DESC, modified DESC";
+ String searchUncategorizedLexicographically = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, " + imagePreviewPath + ", 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) AND category = '' ORDER BY favorite DESC, title COLLATE LOCALIZED ASC";
+ String searchCategoryByModified = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, " + imagePreviewPath + ", 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) AND (category = :category OR category LIKE :category || '/%') ORDER BY category, favorite DESC, modified DESC";
+ String searchCategoryLexicographically = "SELECT id, remoteId, accountId, title, favorite, isShared, readonly, excerpt, modified, category, status, '' as eTag, " + imagePreviewPath + ", 0 as scrollY FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND (title LIKE :query OR content LIKE :query) AND (category = :category OR category LIKE :category || '/%') ORDER BY category, favorite DESC, title COLLATE LOCALIZED ASC";
@Query(getNoteById)
LiveData getNoteById$(long id);
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/NoteImageHelper.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/NoteImageHelper.java
new file mode 100644
index 000000000..0e4ba7b43
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/NoteImageHelper.java
@@ -0,0 +1,164 @@
+package it.niedermann.owncloud.notes.shared.util;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.nextcloud.android.sso.aidl.NextcloudRequest;
+import com.nextcloud.android.sso.api.NextcloudAPI;
+import com.nextcloud.android.sso.model.SingleSignOnAccount;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.UUID;
+
+import it.niedermann.owncloud.notes.persistence.ApiProvider;
+import it.niedermann.owncloud.notes.persistence.NotesRepository;
+import it.niedermann.owncloud.notes.shared.model.ApiVersion;
+
+public class NoteImageHelper {
+ private static final String TAG = "NoteImageHelper";
+
+ /**
+ * Resolves the notes folder path on the Nextcloud server (defaults to "Notes").
+ */
+ @NonNull
+ public static String getNotesPath(@NonNull Context context, @NonNull SingleSignOnAccount ssoAccount, @Nullable NotesRepository repo) {
+ if (repo == null) {
+ repo = NotesRepository.getInstance(context.getApplicationContext());
+ }
+ try {
+ final var call = repo.getServerSettings(ssoAccount, ApiVersion.API_VERSION_1_0);
+ final var response = call.execute();
+ final var settings = response.body();
+ if (settings != null && settings.getNotesPath() != null && !settings.getNotesPath().isEmpty()) {
+ return settings.getNotesPath();
+ }
+ } catch (Exception e) {
+ Log.w(TAG, "Failed to query server settings for notes path, falling back to 'Notes'", e);
+ }
+ return "Notes";
+ }
+
+ /**
+ * Downsamples, compresses, and uploads an image to the Nextcloud WebDAV attachments folder.
+ * Automatically resolves the notes path.
+ *
+ * @return The relative attachment path, e.g. "attachments/abc-123.jpg"
+ */
+ @NonNull
+ public static String uploadImage(@NonNull Context context,
+ @NonNull SingleSignOnAccount ssoAccount,
+ @NonNull Uri imageUri) throws Exception {
+ String notesPath = getNotesPath(context, ssoAccount, null);
+ return uploadImage(context, ssoAccount, notesPath, imageUri);
+ }
+
+ /**
+ * Downsamples, compresses, and uploads an image to the Nextcloud WebDAV attachments folder.
+ *
+ * @return The relative attachment path, e.g. "attachments/abc-123.jpg"
+ */
+ @NonNull
+ public static String uploadImage(@NonNull Context context,
+ @NonNull SingleSignOnAccount ssoAccount,
+ @NonNull String notesPath,
+ @NonNull Uri imageUri) throws Exception {
+ final ContentResolver cr = context.getContentResolver();
+
+ // 1. Determine format
+ String mimeType = cr.getType(imageUri);
+ boolean isPng = mimeType != null && mimeType.contains("png");
+ String extension = isPng ? ".png" : ".jpg";
+ String filename = UUID.randomUUID().toString() + extension;
+
+ // 2. Safely decode bounds
+ BitmapFactory.Options boundsOpts = new BitmapFactory.Options();
+ boundsOpts.inJustDecodeBounds = true;
+ try (InputStream is = cr.openInputStream(imageUri)) {
+ if (is == null) throw new IOException("Cannot open input stream for " + imageUri);
+ BitmapFactory.decodeStream(is, null, boundsOpts);
+ }
+
+ // 3. Compute sample size (target max 1920px)
+ int sampleSize = 1;
+ int maxDim = 1920;
+ while (boundsOpts.outWidth / sampleSize / 2 >= maxDim || boundsOpts.outHeight / sampleSize / 2 >= maxDim) {
+ sampleSize *= 2;
+ }
+
+ // 4. Decode bitmap
+ BitmapFactory.Options decodeOpts = new BitmapFactory.Options();
+ decodeOpts.inSampleSize = sampleSize;
+ Bitmap bm;
+ try (InputStream is = cr.openInputStream(imageUri)) {
+ if (is == null) throw new IOException("Cannot open input stream for " + imageUri);
+ bm = BitmapFactory.decodeStream(is, null, decodeOpts);
+ }
+ if (bm == null) throw new IOException("Failed to decode image bitmap");
+
+ // 5. Scale down if either dimension still exceeds maxDim
+ int largestDimension = Math.max(bm.getWidth(), bm.getHeight());
+ if (largestDimension > maxDim) {
+ float ratio = (float) maxDim / largestDimension;
+ int targetWidth = Math.round(bm.getWidth() * ratio);
+ int targetHeight = Math.round(bm.getHeight() * ratio);
+ bm = Bitmap.createScaledBitmap(bm, targetWidth, targetHeight, true);
+ }
+
+ // 6. Compress to bytes
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ Bitmap.CompressFormat format = isPng ? Bitmap.CompressFormat.PNG : Bitmap.CompressFormat.JPEG;
+ bm.compress(format, 85, baos);
+ byte[] imageBytes = baos.toByteArray();
+ bm.recycle();
+
+ // 7. Get Nextcloud SSO API
+ NextcloudAPI api = ApiProvider.getInstance().getNextcloudAPI(context, ssoAccount);
+
+ // Ensure attachments directory exists (MKCOL)
+ String attachmentsUrl = "/remote.php/webdav/" + notesPath + "/attachments";
+ try {
+ NextcloudRequest mkcol = new NextcloudRequest.Builder()
+ .setMethod("MKCOL")
+ .setUrl(attachmentsUrl)
+ .build();
+ api.performNetworkRequestV2(mkcol);
+ Log.d(TAG, "Created attachments folder: " + attachmentsUrl);
+ } catch (Exception e) {
+ // Folder likely already exists
+ Log.d(TAG, "MKCOL returned (likely exists): " + e.getMessage());
+ }
+
+ // Upload the file (PUT)
+ String fileUrl = attachmentsUrl + "/" + filename;
+ NextcloudRequest put = new NextcloudRequest.Builder()
+ .setMethod("PUT")
+ .setUrl(fileUrl)
+ .setRequestBodyAsStream(new ByteArrayInputStream(imageBytes))
+ .build();
+ api.performNetworkRequestV2(put);
+ Log.i(TAG, "Uploaded image successfully: " + fileUrl + " (" + imageBytes.length + " bytes)");
+
+ return "attachments/" + filename;
+ }
+
+ /**
+ * Formats a Markdown image tag from an attachment path.
+ */
+ @NonNull
+ public static String formatMarkdownImage(@NonNull String attachmentPath) {
+ String filename = attachmentPath.contains("/")
+ ? attachmentPath.substring(attachmentPath.lastIndexOf('/') + 1)
+ : attachmentPath;
+ return "\n\n\n\n";
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/util/NoteImagePreviewLoader.kt b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/NoteImagePreviewLoader.kt
new file mode 100644
index 000000000..25f54a240
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/util/NoteImagePreviewLoader.kt
@@ -0,0 +1,137 @@
+/*
+ * 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.shared.util
+
+import android.content.Context
+import android.net.Uri
+import android.graphics.Bitmap
+import android.util.Log
+import android.view.View
+import android.widget.ImageView
+import com.bumptech.glide.Glide
+import com.nextcloud.android.sso.helper.SingleAccountHelper
+import com.nextcloud.android.sso.model.SingleSignOnAccount
+import it.niedermann.nextcloud.sso.glide.SingleSignOnUrl
+import java.util.concurrent.ExecutionException
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.Executors
+
+object NoteImagePreviewLoader {
+ private const val TAG = "NoteImagePreviewLoader"
+ private val notesPaths = ConcurrentHashMap()
+ private val executor = Executors.newSingleThreadExecutor()
+
+ @JvmStatic
+ fun load(context: Context, content: String, imageView: ImageView): Boolean {
+ val attachmentPath = imageReference(content) ?: run {
+ clear(imageView)
+ return false
+ }
+ val account = SingleAccountHelper.getCurrentSingleSignOnAccount(context) ?: run {
+ clear(imageView)
+ return false
+ }
+ val requestKey = "${account.name}:$attachmentPath"
+ imageView.tag = requestKey
+ imageView.visibility = View.GONE
+ Glide.with(imageView).clear(imageView)
+
+ executor.execute {
+ val notesPath = notesPaths.getOrPut(account.name) {
+ NoteImageHelper.getNotesPath(context.applicationContext, account, null)
+ }
+ val imageUrl = webDavUrl(account, notesPath, attachmentPath)
+ imageView.post {
+ if (imageView.tag != requestKey) return@post
+ imageView.visibility = View.VISIBLE
+ Glide.with(imageView)
+ .load(SingleSignOnUrl(account.name, imageUrl))
+ .centerCrop()
+ .into(imageView)
+ }
+ }
+ return true
+ }
+
+ @JvmStatic
+ fun hasImagePreview(content: String): Boolean = imageReference(content) != null
+
+ @JvmStatic
+ fun loadBitmap(context: Context, content: String, width: Int, height: Int): Bitmap? {
+ val attachmentPath = imageReference(content) ?: return null
+ val account = SingleAccountHelper.getCurrentSingleSignOnAccount(context) ?: return null
+ val notesPath = try {
+ notesPaths.getOrPut(account.name) {
+ NoteImageHelper.getNotesPath(context.applicationContext, account, null)
+ }
+ } catch (exception: Exception) {
+ Log.e(TAG, "Unable to resolve notes path for widget image preview", exception)
+ return null
+ }
+
+ return try {
+ Glide.with(context.applicationContext)
+ .asBitmap()
+ .load(SingleSignOnUrl(account.name, webDavUrl(account, notesPath, attachmentPath)))
+ .centerCrop()
+ .submit(width, height)
+ .get()
+ } catch (exception: InterruptedException) {
+ Thread.currentThread().interrupt()
+ Log.w(TAG, "Widget image preview loading was interrupted", exception)
+ null
+ } catch (exception: ExecutionException) {
+ Log.w(TAG, "Unable to load widget image preview", exception)
+ null
+ }
+ }
+
+ private fun imageReference(content: String): String? {
+ val pathStart = content.indexOf(ATTACHMENTS_PATH)
+ if (pathStart < 0) {
+ return null
+ }
+
+ var pathEnd = content.length
+ for (index in pathStart until content.length) {
+ if (content[index].isWhitespace() || content[index] == ')' || content[index] == '"' || content[index] == '\'') {
+ pathEnd = index
+ break
+ }
+ }
+ return content.substring(pathStart, pathEnd)
+ .takeIf { it.length > ATTACHMENTS_PATH.length }
+ }
+
+ private fun clear(imageView: ImageView) {
+ imageView.tag = null
+ imageView.visibility = View.GONE
+ Glide.with(imageView).clear(imageView)
+ }
+
+ private fun webDavUrl(account: SingleSignOnAccount, notesPath: String, attachmentPath: String): String =
+ Uri.parse(account.url).buildUpon()
+ .appendPath(REMOTE_PATH)
+ .appendPath(WEBDAV_PATH)
+ .appendPathSegments(notesPath)
+ .appendPathSegments(attachmentPath)
+ .build()
+ .toString()
+
+ private fun Uri.Builder.appendPathSegments(path: String): Uri.Builder =
+ apply {
+ Uri.parse(path).pathSegments
+ .filter { it.isNotEmpty() && it != CURRENT_DIRECTORY && it != PARENT_DIRECTORY }
+ .forEach(::appendPath)
+ }
+
+ private const val ATTACHMENTS_PATH = "attachments/"
+ private const val REMOTE_PATH = "remote.php"
+ private const val WEBDAV_PATH = "webdav"
+ private const val CURRENT_DIRECTORY = "."
+ private const val PARENT_DIRECTORY = ".."
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidget.kt b/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidget.kt
index fd15fa597..a87d171bf 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidget.kt
+++ b/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidget.kt
@@ -13,13 +13,16 @@ import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.net.Uri
+import android.os.Build
import android.util.Log
import android.widget.RemoteViews
import com.owncloud.android.lib.common.utils.Log_OC
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.shared.util.WidgetUtil
+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 java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import androidx.core.net.toUri
@@ -97,14 +100,21 @@ class NoteListWidget : AppWidgetProvider() {
setPackage(context.packageName)
}
- val pendingIntentFlags =
- WidgetUtil.pendingIntentFlagCompat(PendingIntent.FLAG_UPDATE_CURRENT or Intent.FILL_IN_COMPONENT)
+ val pendingIntentFlags = PendingIntent.FLAG_UPDATE_CURRENT or
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0
val editNotePendingIntent =
- PendingIntent.getActivity(context, 0, editNoteIntent, pendingIntentFlags)
+ PendingIntent.getActivity(context, appWidgetId, editNoteIntent, pendingIntentFlags)
+ val createNotePendingIntent = PendingIntent.getActivity(
+ context,
+ appWidgetId,
+ getCreateNoteIntent(context, data),
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
val views = RemoteViews(context.packageName, R.layout.widget_note_list).apply {
setRemoteAdapter(R.id.note_list_widget_lv, serviceIntent)
setPendingIntentTemplate(R.id.note_list_widget_lv, editNotePendingIntent)
+ setOnClickPendingIntent(R.id.widget_add_note, createNotePendingIntent)
setEmptyView(
R.id.note_list_widget_lv,
R.id.widget_note_list_placeholder_tv
@@ -126,5 +136,22 @@ class NoteListWidget : AppWidgetProvider() {
}
context.sendBroadcast(intent)
}
+
+ private fun getCreateNoteIntent(
+ context: Context,
+ data: NotesListWidgetData
+ ): Intent {
+ val navigationCategory = if (data.mode == NotesListWidgetData.MODE_DISPLAY_STARRED) {
+ NavigationCategory(ENavigationCategoryType.FAVORITES)
+ } else {
+ NavigationCategory(data.accountId, data.category)
+ }
+
+ return Intent(context, EditNoteActivity::class.java).apply {
+ setPackage(context.packageName)
+ putExtra(EditNoteActivity.PARAM_CATEGORY, navigationCategory)
+ putExtra(EditNoteActivity.PARAM_ACCOUNT_ID, data.accountId)
+ }
+ }
}
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidgetFactory.kt b/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidgetFactory.kt
index ab5123bd7..7a90fb9b7 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidgetFactory.kt
+++ b/app/src/main/java/it/niedermann/owncloud/notes/widget/notelist/NoteListWidgetFactory.kt
@@ -20,11 +20,9 @@ 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.Account
import it.niedermann.owncloud.notes.persistence.entity.Note
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.NoteImagePreviewLoader
class NoteListWidgetFactory internal constructor(private val context: Context, intent: Intent) :
RemoteViewsFactory {
@@ -101,21 +99,6 @@ class NoteListWidgetFactory internal constructor(private val context: Context, i
}
}
- private fun getCreateNoteIntent(localAccount: Account): Intent {
- val bundle = Bundle()
-
- data?.let {
- val navigationCategory = if (it.mode == NotesListWidgetData.MODE_DISPLAY_STARRED) NavigationCategory(
- ENavigationCategoryType.FAVORITES
- ) else NavigationCategory(localAccount.id, it.category)
-
- bundle.putSerializable(EditNoteActivity.PARAM_CATEGORY, navigationCategory)
- bundle.putLong(EditNoteActivity.PARAM_ACCOUNT_ID, it.accountId)
- }
-
- return getEditNoteIntent(bundle)
- }
-
private fun getOpenNoteIntent(note: Note): Intent {
val bundle = Bundle().apply {
putLong(EditNoteActivity.PARAM_NOTE_ID, note.id)
@@ -130,20 +113,20 @@ class NoteListWidgetFactory internal constructor(private val context: Context, i
val openNoteIntent = getOpenNoteIntent(note)
- var createNoteIntent: Intent? = null
- data?.let {
- val localAccount = repo.getAccountById(it.accountId)
- createNoteIntent = getCreateNoteIntent(localAccount)
- }
-
- return RemoteViews(context.packageName, R.layout.widget_entry).apply {
+ return RemoteViews(context.packageName, R.layout.widget_entry_grid).apply {
setOnClickFillInIntent(R.id.widget_note_list_entry, openNoteIntent)
- createNoteIntent?.let {
- setOnClickFillInIntent(R.id.widget_entry_fav_icon, createNoteIntent)
- }
-
setTextViewText(R.id.widget_entry_title, note.title)
+ setTextViewText(R.id.widget_entry_excerpt, note.excerpt.replace("\uFFFC", "").trim())
+ val thumbnailSize = context.resources.getDimensionPixelSize(
+ R.dimen.widget_note_list_thumbnail_size
+ )
+ NoteImagePreviewLoader.loadBitmap(context, note.content, thumbnailSize, thumbnailSize)
+ ?.let { image ->
+ setImageViewBitmap(R.id.widget_entry_image_preview, image)
+ setViewVisibility(R.id.widget_entry_image_preview, View.VISIBLE)
+ }
+ ?: setViewVisibility(R.id.widget_entry_image_preview, View.GONE)
if (note.category.isEmpty()) {
setViewVisibility(R.id.widget_entry_category, View.GONE)
@@ -160,12 +143,6 @@ class NoteListWidgetFactory internal constructor(private val context: Context, i
setTextColor(R.id.widget_entry_category, textColor)
}
- val starIconId = if (note.favorite) {
- R.drawable.ic_star_yellow_24dp
- } else {
- R.drawable.ic_star_grey_ccc_24dp
- }
- setImageViewResource(R.id.widget_entry_fav_icon, starIconId)
}
}
diff --git a/app/src/main/res/drawable/ic_image_white_24dp.xml b/app/src/main/res/drawable/ic_image_white_24dp.xml
new file mode 100644
index 000000000..1b4128c53
--- /dev/null
+++ b/app/src/main/res/drawable/ic_image_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/layout/item_notes_list_note_item_grid.xml b/app/src/main/res/layout/item_notes_list_note_item_grid.xml
index 2191dba0c..f76221430 100644
--- a/app/src/main/res/layout/item_notes_list_note_item_grid.xml
+++ b/app/src/main/res/layout/item_notes_list_note_item_grid.xml
@@ -33,6 +33,16 @@
tools:maxLength="50"
tools:text="@tools:sample/lorem/random" />
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/widget_entry_grid.xml b/app/src/main/res/layout/widget_entry_grid.xml
new file mode 100644
index 000000000..a3262b1cc
--- /dev/null
+++ b/app/src/main/res/layout/widget_entry_grid.xml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/widget_note_list.xml b/app/src/main/res/layout/widget_note_list.xml
index c91ab2e55..45999b3a0 100644
--- a/app/src/main/res/layout/widget_note_list.xml
+++ b/app/src/main/res/layout/widget_note_list.xml
@@ -5,23 +5,37 @@
~ SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
~ SPDX-License-Identifier: GPL-3.0-or-later
-->
-
-
+ android:verticalSpacing="@dimen/widget_note_list_outer_padding"
+ tools:listitem="@layout/widget_entry_grid" />
+
+
-
+
diff --git a/app/src/main/res/menu/menu_note_fragment.xml b/app/src/main/res/menu/menu_note_fragment.xml
index e4a7bfa35..352e6f6f2 100644
--- a/app/src/main/res/menu/menu_note_fragment.xml
+++ b/app/src/main/res/menu/menu_note_fragment.xml
@@ -16,6 +16,12 @@
app:actionViewClass="androidx.appcompat.widget.SearchView"
app:showAsAction="ifRoom|collapseActionView" />
+
- 100dp
18dp
+ 56dp
+ 144dp
40dp
16dp
@@ -76,6 +78,7 @@
4dp
26dp
20dp
+ 56dp
48dp
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 80d4284fc..e1914a4e8 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -30,6 +30,9 @@
Delete
Category
Favorite
+ Attach image
+ Uploading image…
+ Image attached
Preview
Share
Shared note
diff --git a/app/src/main/res/xml/note_list_widget_provider_info.xml b/app/src/main/res/xml/note_list_widget_provider_info.xml
index c2098988f..d09bdb36f 100644
--- a/app/src/main/res/xml/note_list_widget_provider_info.xml
+++ b/app/src/main/res/xml/note_list_widget_provider_info.xml
@@ -15,6 +15,8 @@
android:minResizeHeight="40dp"
android:minResizeWidth="80dp"
android:resizeMode="vertical|horizontal"
+ android:targetCellHeight="9"
+ android:targetCellWidth="5"
android:updatePeriodMillis="86400000"
android:widgetCategory="keyguard|home_screen"
android:previewImage="@drawable/note_list_widget_preview" />
diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml
index 637abf1bc..cb4c0586c 100644
--- a/app/src/main/res/xml/preferences.xml
+++ b/app/src/main/res/xml/preferences.xml
@@ -69,6 +69,7 @@
android:summary="@string/settings_enable_direct_edit_summary" />