mirror of
https://github.com/nextcloud/notes-android.git
synced 2024-11-22 12:56:02 +03:00
Merge branch 'master' of github.com:stefan-niedermann/nextcloud-notes into markdown-context-menu-multiline
This commit is contained in:
commit
f3c40e5108
30 changed files with 543 additions and 120 deletions
40
FAQ.md
40
FAQ.md
|
@ -27,17 +27,45 @@ Sorry. There are so many different environments, that it is impossible for us to
|
|||
|
||||
First of all make sure you have updated to and tried with the latest available versions of both, this app and the [Notes server app](https://apps.nextcloud.com/apps/notes).
|
||||
|
||||
In case you receive a `NextcloudApiNotRespondingException`, try to disable the battery optimization for both apps.
|
||||
In all other cases please try to clear the storage of **both** apps, Nextcloud Android **and** Nextcloud Notes Android.
|
||||
### `NextcloudApiNotRespondingException`
|
||||
|
||||
Try to disable the battery "optimization" for both apps. Some manufacturers prevent the app from communicating with the Nextcloud Android properly.
|
||||
This is a [known issue of the SingleSignOn mechanism](https://github.com/nextcloud/Android-SingleSignOn#troubleshooting) which we only can work around but not solve on our side.
|
||||
|
||||
### `UnknownErrorException: Read timed out`
|
||||
|
||||
This issue is caused by a connection time out. This can be the case if there are infrastructural or environmental problems (like a misconfigured server or a bad network connection).
|
||||
Probably you will experience it when importing an account, because at this moment, all your Notes will getting downloaded at once. Given you have a lots of notes, this might take longer than the connection is available.
|
||||
Further synchronizations are usually not causing this issue, because the Notes app tries to synchronize only *changed* notes after the first import.
|
||||
If your notes are not ten thousands of characters long, it is very unlikely that this causes a connection timeout.
|
||||
|
||||
We plan to improve the import of an account and make it more reliable by [fetching notes step by step](https://github.com/stefan-niedermann/nextcloud-notes/issues/761#issuecomment-836989421) in a future release.
|
||||
Until then you can as a workaround for the first import try to
|
||||
1. move all your notes to a different folder on your Nextcloud instance
|
||||
2. import your account on your smartphone
|
||||
3. put your notes back to the original folder step by step and sync everytime you put some notes back
|
||||
|
||||
### `NextcloudFilesAppAccountNotFoundException`
|
||||
|
||||
We are not yet sure what exactly causes this issue, but investigate it by [adding more debug logs to recent versions](https://github.com/stefan-niedermann/nextcloud-notes/issues/1256#issuecomment-859505153). In theory this might happen if an already imported account has been deleted in the Nextcloud app.
|
||||
As a workaround you can remove the account (or clear the storage of the app as described below if you can't access the account manager anymore) and import it again.
|
||||
|
||||
### `TokenMismatchException` and all others
|
||||
|
||||
In all other cases please try to clear the storage of **both** apps, Nextcloud Android **and** Nextcloud Notes Android. Not yet synchronized changes will be lost by performing this step.
|
||||
|
||||
You can achieve this by navigating to
|
||||
|
||||
```
|
||||
Android settings
|
||||
↳ Apps
|
||||
↳ Nextcloud / Notes
|
||||
↳ Storage
|
||||
↳ Clear storage
|
||||
↓
|
||||
Apps
|
||||
↓
|
||||
Nextcloud / Notes
|
||||
↓
|
||||
Storage
|
||||
↓
|
||||
Clear storage
|
||||
```
|
||||
|
||||
Then set up your account in the Nextcloud Android app again and import the configured account in the Nextcloud Notes Android app.
|
||||
|
|
|
@ -12,8 +12,8 @@ android {
|
|||
applicationId "it.niedermann.owncloud.notes"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 30
|
||||
versionCode 3004009
|
||||
versionName "3.4.9"
|
||||
versionCode 3004010
|
||||
versionName "3.4.10"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
javaCompileOptions {
|
||||
annotationProcessorOptions {
|
||||
|
@ -86,7 +86,7 @@ dependencies {
|
|||
|
||||
// Android X
|
||||
implementation 'androidx.appcompat:appcompat:1.3.0'
|
||||
implementation 'androidx.fragment:fragment:1.3.4'
|
||||
implementation 'androidx.fragment:fragment:1.3.5'
|
||||
implementation 'androidx.preference:preference:1.1.1'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.2.1'
|
||||
implementation 'androidx.recyclerview:recyclerview-selection:1.1.0'
|
||||
|
@ -112,7 +112,7 @@ dependencies {
|
|||
testImplementation 'androidx.test:core:1.3.0'
|
||||
testImplementation 'androidx.arch.core:core-testing:2.1.0'
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation 'org.mockito:mockito-core:3.11.0'
|
||||
testImplementation 'org.mockito:mockito-core:3.11.1'
|
||||
testImplementation 'org.robolectric:robolectric:4.5.1'
|
||||
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
|
|
|
@ -29,6 +29,7 @@ import it.niedermann.owncloud.notes.R;
|
|||
import it.niedermann.owncloud.notes.databinding.FragmentNoteEditBinding;
|
||||
import it.niedermann.owncloud.notes.persistence.entity.Note;
|
||||
import it.niedermann.owncloud.notes.shared.model.ISyncCallback;
|
||||
import it.niedermann.owncloud.notes.shared.util.DisplayUtils;
|
||||
|
||||
import static androidx.core.view.ViewCompat.isAttachedToWindow;
|
||||
import static it.niedermann.owncloud.notes.shared.util.NoteUtil.getFontSizeFromPreferences;
|
||||
|
@ -59,6 +60,7 @@ public class NoteEditFragment extends SearchableBaseNoteFragment {
|
|||
}
|
||||
};
|
||||
private TextWatcher textWatcher;
|
||||
private boolean keyboardShown = false;
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
|
@ -138,22 +140,17 @@ public class NoteEditFragment extends SearchableBaseNoteFragment {
|
|||
public void onResume() {
|
||||
super.onResume();
|
||||
binding.editContent.addTextChangedListener(textWatcher);
|
||||
|
||||
if (keyboardShown) {
|
||||
openSoftKeyboard();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNoteLoaded(Note note) {
|
||||
super.onNoteLoaded(note);
|
||||
if (TextUtils.isEmpty(note.getContent())) {
|
||||
binding.editContent.post(() -> {
|
||||
binding.editContent.requestFocus();
|
||||
|
||||
final InputMethodManager imm = (InputMethodManager) requireContext().getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
if (imm != null) {
|
||||
imm.showSoftInput(binding.editContent, InputMethodManager.SHOW_IMPLICIT);
|
||||
} else {
|
||||
Log.e(TAG, InputMethodManager.class.getSimpleName() + " is null.");
|
||||
}
|
||||
});
|
||||
openSoftKeyboard();
|
||||
}
|
||||
|
||||
binding.editContent.setMarkdownString(note.getContent());
|
||||
|
@ -166,11 +163,32 @@ public class NoteEditFragment extends SearchableBaseNoteFragment {
|
|||
}
|
||||
}
|
||||
|
||||
private void openSoftKeyboard() {
|
||||
binding.editContent.postDelayed(() -> {
|
||||
binding.editContent.requestFocus();
|
||||
|
||||
final InputMethodManager imm = (InputMethodManager) requireContext().getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
if (imm != null) {
|
||||
imm.showSoftInput(binding.editContent, InputMethodManager.SHOW_IMPLICIT);
|
||||
} else {
|
||||
Log.e(TAG, InputMethodManager.class.getSimpleName() + " is null.");
|
||||
}
|
||||
//Without a small delay the keyboard does not show reliably
|
||||
}, 100);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
binding.editContent.removeTextChangedListener(textWatcher);
|
||||
cancelTimers();
|
||||
|
||||
final ViewGroup parentView = requireActivity().findViewById(android.R.id.content);
|
||||
if (parentView != null && parentView.getChildCount() > 0) {
|
||||
keyboardShown = DisplayUtils.isSoftKeyboardVisible(parentView.getChildAt(0));
|
||||
} else {
|
||||
keyboardShown = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void cancelTimers() {
|
||||
|
|
|
@ -15,9 +15,11 @@ import android.util.Log;
|
|||
import android.view.View;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.view.ActionMode;
|
||||
import androidx.appcompat.widget.SearchView;
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout;
|
||||
|
@ -50,9 +52,11 @@ import com.nextcloud.android.sso.helper.SingleAccountHelper;
|
|||
import java.net.HttpURLConnection;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import it.niedermann.owncloud.notes.LockedActivity;
|
||||
|
@ -90,6 +94,7 @@ import it.niedermann.owncloud.notes.shared.model.NavigationCategory;
|
|||
import it.niedermann.owncloud.notes.shared.model.NoteClickListener;
|
||||
import it.niedermann.owncloud.notes.shared.util.CustomAppGlideModule;
|
||||
import it.niedermann.owncloud.notes.shared.util.NoteUtil;
|
||||
import it.niedermann.owncloud.notes.shared.util.ShareUtil;
|
||||
|
||||
import static android.os.Build.VERSION.SDK_INT;
|
||||
import static android.os.Build.VERSION_CODES.O;
|
||||
|
@ -175,25 +180,52 @@ public class MainActivity extends LockedActivity implements NoteClickListener, A
|
|||
try {
|
||||
final Account account = mainViewModel.getLocalAccountByAccountName(SingleAccountHelper.getCurrentSingleSignOnAccount(getApplicationContext()).name);
|
||||
runOnUiThread(() -> mainViewModel.postCurrentAccount(account));
|
||||
} catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) {
|
||||
} catch (NextcloudFilesAppAccountNotFoundException e) {
|
||||
// Verbose log output for https://github.com/stefan-niedermann/nextcloud-notes/issues/1256
|
||||
final Throwable exception;
|
||||
if (e instanceof NextcloudFilesAppAccountNotFoundException) {
|
||||
final SharedPreferences ssoPreferences = AccountImporter.getSharedPreferences(getApplicationContext());
|
||||
final StringBuilder ssoPreferencesString = new StringBuilder()
|
||||
.append("Current SSO account: ").append(ssoPreferences.getString("PREF_CURRENT_ACCOUNT_STRING", null)).append("\n")
|
||||
.append("\n")
|
||||
.append("SSO SharedPreferences: ").append("\n");
|
||||
for (Map.Entry<String, ?> entry : ssoPreferences.getAll().entrySet()) {
|
||||
ssoPreferencesString.append(entry.getKey()).append(": ").append(entry.getValue()).append("\n");
|
||||
}
|
||||
ssoPreferencesString.append("\n")
|
||||
.append("Available accounts in DB: ").append(TextUtils.join(", ", mainViewModel.getAccounts().stream().map(Account::getAccountName).collect(Collectors.toList())));
|
||||
exception = new RuntimeException(((NextcloudFilesAppAccountNotFoundException) e).getMessage(), new RuntimeException(ssoPreferencesString.toString(), e));
|
||||
} else {
|
||||
exception = e;
|
||||
}
|
||||
runOnUiThread(() -> ExceptionDialogFragment.newInstance(exception).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()));
|
||||
runOnUiThread(() -> new AlertDialog.Builder(this)
|
||||
.setTitle(NextcloudFilesAppAccountNotFoundException.class.getSimpleName())
|
||||
.setMessage(R.string.backup_and_repair)
|
||||
.setPositiveButton(R.string.simple_repair, (a, b) -> {
|
||||
executor.submit(() -> {
|
||||
for (Account account : mainViewModel.getAccounts()) {
|
||||
SingleAccountHelper.setCurrentAccount(this, account.getAccountName());
|
||||
runOnUiThread(this::recreate);
|
||||
break;
|
||||
}
|
||||
});
|
||||
})
|
||||
.setNegativeButton(R.string.simple_backup, (a, b) -> {
|
||||
executor.submit(() -> {
|
||||
final List<Note> modifiedNotes = new LinkedList<>();
|
||||
for (Account account : mainViewModel.getAccounts()) {
|
||||
modifiedNotes.addAll(mainViewModel.getLocalModifiedNotes(account.getId()));
|
||||
}
|
||||
if (modifiedNotes.size() == 1) {
|
||||
final Note note = modifiedNotes.get(0);
|
||||
ShareUtil.openShareDialog(this, note.getTitle(), note.getContent());
|
||||
} else {
|
||||
ShareUtil.openShareDialog(this,
|
||||
getResources().getQuantityString(R.plurals.share_multiple, modifiedNotes.size(), modifiedNotes.size()),
|
||||
mainViewModel.collectNoteContents(modifiedNotes.stream().map(Note::getId).collect(Collectors.toList())));
|
||||
}
|
||||
});
|
||||
})
|
||||
.setNeutralButton(android.R.string.cancel, (a, b) -> {
|
||||
final SharedPreferences ssoPreferences = AccountImporter.getSharedPreferences(getApplicationContext());
|
||||
final StringBuilder ssoPreferencesString = new StringBuilder()
|
||||
.append("Current SSO account: ").append(ssoPreferences.getString("PREF_CURRENT_ACCOUNT_STRING", null)).append("\n")
|
||||
.append("\n")
|
||||
.append("SSO SharedPreferences: ").append("\n");
|
||||
for (Map.Entry<String, ?> entry : ssoPreferences.getAll().entrySet()) {
|
||||
ssoPreferencesString.append(entry.getKey()).append(": ").append(entry.getValue()).append("\n");
|
||||
}
|
||||
ssoPreferencesString.append("\n")
|
||||
.append("Available accounts in DB: ").append(TextUtils.join(", ", mainViewModel.getAccounts().stream().map(Account::getAccountName).collect(Collectors.toList())));
|
||||
runOnUiThread(() -> ExceptionDialogFragment.newInstance(new RuntimeException(e.getMessage(), new RuntimeException(ssoPreferencesString.toString(), e))).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()));
|
||||
})
|
||||
.show());
|
||||
} catch (NoCurrentAccountSelectedException e) {
|
||||
runOnUiThread(() -> ExceptionDialogFragment.newInstance(e).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -744,6 +776,8 @@ public class MainActivity extends LockedActivity implements NoteClickListener, A
|
|||
public void onBackPressed() {
|
||||
if (activityBinding.toolbar.getVisibility() == VISIBLE) {
|
||||
updateToolbars(true);
|
||||
} else if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
|
||||
binding.drawerLayout.closeDrawer(GravityCompat.START);
|
||||
} else {
|
||||
super.onBackPressed();
|
||||
}
|
||||
|
|
|
@ -594,6 +594,10 @@ public class MainViewModel extends AndroidViewModel {
|
|||
repo.createOrUpdateSingleNoteWidgetData(data);
|
||||
}
|
||||
|
||||
public List<Note> getLocalModifiedNotes(long accountId) {
|
||||
return repo.getLocalModifiedNotes(accountId);
|
||||
}
|
||||
|
||||
public LiveData<Integer> getAccountsCount() {
|
||||
return repo.countAccounts$();
|
||||
}
|
||||
|
|
|
@ -2,27 +2,21 @@ package it.niedermann.owncloud.notes.shared.util;
|
|||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Color;
|
||||
import android.text.Spannable;
|
||||
import android.text.TextPaint;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.MetricAffectingSpan;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Build;
|
||||
import android.util.TypedValue;
|
||||
import android.view.View;
|
||||
import android.view.WindowInsets;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import it.niedermann.android.util.ColorUtil;
|
||||
import it.niedermann.owncloud.notes.NotesApplication;
|
||||
import it.niedermann.owncloud.notes.R;
|
||||
import it.niedermann.owncloud.notes.branding.BrandingUtil;
|
||||
import it.niedermann.owncloud.notes.main.navigation.NavigationAdapter;
|
||||
import it.niedermann.owncloud.notes.main.navigation.NavigationItem;
|
||||
import it.niedermann.owncloud.notes.persistence.entity.CategoryWithNotesCount;
|
||||
|
@ -52,4 +46,29 @@ public class DisplayUtils {
|
|||
}
|
||||
return new NavigationItem.CategoryNavigationItem("category:" + counter.getCategory(), counter.getCategory(), counter.getTotalNotes(), icon, counter.getAccountId(), counter.getCategory());
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if the soft keyboard is open.
|
||||
* On API prior to 30 we fall back to workaround which might be less reliable
|
||||
*
|
||||
* @param parentView View
|
||||
* @return keyboardVisibility Boolean
|
||||
*/
|
||||
public static boolean isSoftKeyboardVisible(@NonNull View parentView) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
final WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(parentView);
|
||||
if (insets != null) {
|
||||
return insets.isVisible(WindowInsets.Type.ime());
|
||||
}
|
||||
}
|
||||
|
||||
//Arbitrary keyboard height
|
||||
final int defaultKeyboardHeightDP = 100;
|
||||
final int EstimatedKeyboardDP = defaultKeyboardHeightDP + (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? 48 : 0);
|
||||
final Rect rect = new Rect();
|
||||
final int estimatedKeyboardHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, EstimatedKeyboardDP, parentView.getResources().getDisplayMetrics());
|
||||
parentView.getWindowVisibleDisplayFrame(rect);
|
||||
final int heightDiff = parentView.getRootView().getHeight() - (rect.bottom - rect.top);
|
||||
return heightDiff >= estimatedKeyboardHeight;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -259,4 +259,7 @@
|
|||
<string name="you_have_to_be_connected_to_the_internet_in_order_to_add_an_account">Abyste mohli přidat účet je třeba, abyste byli připojení k Internetu.</string>
|
||||
<string name="simple_next">Další</string>
|
||||
<string name="simple_prev">Předchozí</string>
|
||||
<string name="simple_backup">Zazálohovat</string>
|
||||
<string name="simple_repair">Opravit</string>
|
||||
<string name="backup_and_repair">Doporučujeme zazálohovat veškeré nesesynchronizované poznámky a pak se pokusit opravit nastavení.</string>
|
||||
</resources>
|
||||
|
|
|
@ -249,4 +249,7 @@
|
|||
<string name="you_have_to_be_connected_to_the_internet_in_order_to_add_an_account">Sie müssen mit dem Internet verbunden sein, um ein Konto hinzufügen zu können.</string>
|
||||
<string name="simple_next">Weiter</string>
|
||||
<string name="simple_prev">Vorheriges</string>
|
||||
<string name="simple_backup">Sicherung</string>
|
||||
<string name="simple_repair">Reparieren</string>
|
||||
<string name="backup_and_repair">Wir empfehlen, alle unsynchronisierten Notizen zu sichern und dann zu reparieren.</string>
|
||||
</resources>
|
||||
|
|
|
@ -249,4 +249,4 @@
|
|||
<string name="you_have_to_be_connected_to_the_internet_in_order_to_add_an_account">Tienes que estar conectado a internet para poder añadir una cuenta.</string>
|
||||
<string name="simple_next">Siguiente</string>
|
||||
<string name="simple_prev">Anterior</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
|
|
@ -249,4 +249,4 @@
|
|||
<string name="you_have_to_be_connected_to_the_internet_in_order_to_add_an_account">Kontu bat gehitzeko internetera konektatuta egon behar zara.</string>
|
||||
<string name="simple_next">Hurrengoa</string>
|
||||
<string name="simple_prev">Aurrekoa</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
|
|
@ -245,4 +245,4 @@
|
|||
<string name="you_have_to_be_connected_to_the_internet_in_order_to_add_an_account">Sinun tulee olla yhteydessä internetiin, jotta voit lisätä tilin.</string>
|
||||
<string name="simple_next">Seuraava</string>
|
||||
<string name="simple_prev">Edellinen</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
<string name="action_sorting_method">Méthode de tri</string>
|
||||
<string name="simple_cancel">Annuler</string>
|
||||
<string name="simple_edit">Modifier</string>
|
||||
<string name="simple_remove">Supprimer</string>
|
||||
<string name="action_edit_save">Enregistrer</string>
|
||||
<string name="simple_about">À propos</string>
|
||||
<string name="simple_link">Lien</string>
|
||||
|
|
|
@ -6,12 +6,14 @@
|
|||
<string name="label_all_notes">Minden jegyzet</string>
|
||||
<string name="label_favorites">Kedvencek</string>
|
||||
<string name="action_create">Új jegyzet</string>
|
||||
<string name="welcome_text">Üdvözli a %1$s</string>
|
||||
<string name="action_settings">Beállítások</string>
|
||||
<string name="action_trashbin">Törölt jegyzetek</string>
|
||||
<string name="action_search">Keresés</string>
|
||||
<string name="action_sorting_method">Rendezési mód</string>
|
||||
<string name="simple_cancel">Mégse</string>
|
||||
<string name="simple_edit">Szerkesztés</string>
|
||||
<string name="simple_remove">Eltávolítás</string>
|
||||
<string name="action_edit_save">Mentés</string>
|
||||
<string name="simple_about">Névjegy</string>
|
||||
<string name="simple_link">Hivatkozás</string>
|
||||
|
@ -50,6 +52,7 @@
|
|||
<string name="error_sync">Szinkronizálás sikertelen: %1$s</string>
|
||||
<string name="error_synchronization">Szinkronizálás sikertelen</string>
|
||||
<string name="error_no_network">Nincs hálózati kapcsolat</string>
|
||||
<string name="error_maintenance_mode">A kiszolgáló karbantartási módban van</string>
|
||||
<string name="error_unknown">Ismeretlen hiba történt.</string>
|
||||
|
||||
<string name="about_version_title">Verzió</string>
|
||||
|
@ -184,7 +187,7 @@
|
|||
<string name="formatting_help_codefence_javascript" translateable="false">```javascript</string>
|
||||
<string name="formatting_help_cbf_title">Környezetfüggő formázás</string>
|
||||
<string name="formatting_help_cbf_body_1">A Jegyzetek alkalmazás egyik célkitűzése, hogy egyszerű legyen, és ne vonja el a figyelmet. Viszont Markdownnal formázhatja a szövegeket. A lent említett példák némelyikénél rövidítéseket is használhat, így anélkül formázhatja meg a jegyzeteket, hogy beírná a lenti kódokat.</string>
|
||||
<string name="formatting_help_cbf_body_2">Csak válasszon ki egy szövegtartományt, vagy érintse meg a kurzort bármely helyen, és megjelenik egy előugró menü, amely az alapértelmezett bejegyzések mellett %1$s, %2$s, %3$s tartalmaz olyan bejegyzéseket, mint %4$s vagy %5$s.</string>
|
||||
<string name="formatting_help_cbf_body_2">Csak válasszon ki egy szövegtartományt, vagy érintse meg bárhol a kurzort, és megjelenik egy előugró menü, amely az alapértelmezett %1$s, %2$s, %3$s bejegyzések mellett ezeket is tartalmazza: %4$s, %5$s.</string>
|
||||
|
||||
<string name="formatting_help_text_title">Szöveg</string>
|
||||
<string name="formatting_help_text_body">Nagyon egyszerűen írhat Markdownnal %1$sfélkövéren%1$s, valamint %2$sdőlten%2$s. Át is %3$shúzhat%3$s szavakat, valamint [hivatkozhat a Nextcloudra](https://nextcloud.com).</string>
|
||||
|
@ -235,5 +238,15 @@
|
|||
<string name="error_action_open_network">Hálózati beállítások</string>
|
||||
<string name="no_account_configured_yet">Nincs még fiók beállítva</string>
|
||||
<string name="no_other_accounts">Még egyetlen más fiókot sem állított be.</string>
|
||||
<string name="choose_account">Válasszon fiókot</string>
|
||||
<string name="context_based_formatting">Környezetfüggő formázási felbukkanó menü</string>
|
||||
<plurals name="remove_account_message">
|
||||
<item quantity="one">A(z) %1$s fiók eltávolítása véglegesen töröl egy nem szinkronizált változtatást.</item>
|
||||
<item quantity="other">A(z) %1$s fiók eltávolítása véglegesen töröl %2$d nem szinkronizált változtatást.</item>
|
||||
</plurals>
|
||||
<string name="remove_account">%1$s eltávolítása</string>
|
||||
|
||||
<string name="you_have_to_be_connected_to_the_internet_in_order_to_add_an_account">Internetkapcsolatra van szükség, hogy fiókot adjon hozzá.</string>
|
||||
<string name="simple_next">Következő</string>
|
||||
<string name="simple_prev">Előző</string>
|
||||
</resources>
|
||||
|
|
|
@ -249,4 +249,4 @@
|
|||
<string name="you_have_to_be_connected_to_the_internet_in_order_to_add_an_account">Devi essere connesso a Internet per aggiungere un account.</string>
|
||||
<string name="simple_next">Successivo</string>
|
||||
<string name="simple_prev">Precedente</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
|
|
@ -249,4 +249,4 @@
|
|||
<string name="you_have_to_be_connected_to_the_internet_in_order_to_add_an_account">Je moet verbonden zijn met het Internet om een account toe te voegen.</string>
|
||||
<string name="simple_next">Volgende</string>
|
||||
<string name="simple_prev">Vorige</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
|
|
@ -259,4 +259,7 @@
|
|||
<string name="you_have_to_be_connected_to_the_internet_in_order_to_add_an_account">Aby dodać konto, musisz mieć połączenie z Internetem.</string>
|
||||
<string name="simple_next">Następna</string>
|
||||
<string name="simple_prev">Poprzednia</string>
|
||||
<string name="simple_backup">Kopia zapasowa</string>
|
||||
<string name="simple_repair">Naprawa</string>
|
||||
<string name="backup_and_repair">Zalecane wykonanie kopii zapasowej wszystkich niezsynchronizowanych notatek, a następnie próbę naprawy konfiguracji.</string>
|
||||
</resources>
|
||||
|
|
|
@ -249,4 +249,7 @@
|
|||
<string name="you_have_to_be_connected_to_the_internet_in_order_to_add_an_account">Você deve estar conectado à Internet para adicionar uma conta. </string>
|
||||
<string name="simple_next">Próximo</string>
|
||||
<string name="simple_prev">Anterior</string>
|
||||
<string name="simple_backup">Backup</string>
|
||||
<string name="simple_repair">Reparar</string>
|
||||
<string name="backup_and_repair">Recomendamos fazer backup de todas as notas não sincronizadas e, em seguida, tentar reparar a configuração. </string>
|
||||
</resources>
|
||||
|
|
|
@ -259,4 +259,4 @@
|
|||
<string name="you_have_to_be_connected_to_the_internet_in_order_to_add_an_account">Za dodajanje računa mora biti vzpostavljena povezava z omrežjem.</string>
|
||||
<string name="simple_next">Naslednje</string>
|
||||
<string name="simple_prev">Predhodno</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
|
|
@ -249,4 +249,7 @@
|
|||
<string name="you_have_to_be_connected_to_the_internet_in_order_to_add_an_account">Bir hesap ekleyebilmeniz için çalışan bir İnternet bağlantınız olmalı.</string>
|
||||
<string name="simple_next">Sonraki</string>
|
||||
<string name="simple_prev">Önceki</string>
|
||||
<string name="simple_backup">Yedekle</string>
|
||||
<string name="simple_repair">Onar</string>
|
||||
<string name="backup_and_repair">Eşitlenmemiş tüm notları yedekledikten sonra kurulumu onarmayı denemeniz önerilir.</string>
|
||||
</resources>
|
||||
|
|
|
@ -226,4 +226,4 @@
|
|||
<string name="you_have_to_be_connected_to_the_internet_in_order_to_add_an_account">Bạn phải có kết nối internet để thêm tài khoản.</string>
|
||||
<string name="simple_next">Tiếp</string>
|
||||
<string name="simple_prev">Trước</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
|
|
@ -244,4 +244,7 @@
|
|||
<string name="you_have_to_be_connected_to_the_internet_in_order_to_add_an_account">要添加账号,你必须连接到互联网。</string>
|
||||
<string name="simple_next">下一则 </string>
|
||||
<string name="simple_prev">上一则</string>
|
||||
<string name="simple_backup">备份</string>
|
||||
<string name="simple_repair">修复</string>
|
||||
<string name="backup_and_repair">我们建议备份所有未同步的笔记,然后尝试修复安装。</string>
|
||||
</resources>
|
||||
|
|
|
@ -244,4 +244,7 @@
|
|||
<string name="you_have_to_be_connected_to_the_internet_in_order_to_add_an_account">您必須連線到互聯網才能新增賬戶。</string>
|
||||
<string name="simple_next">下一步</string>
|
||||
<string name="simple_prev">上一個</string>
|
||||
<string name="simple_backup">備份</string>
|
||||
<string name="simple_repair">修復</string>
|
||||
<string name="backup_and_repair">我們建議備份所有未同步的筆記,然後嘗試修復設置。</string>
|
||||
</resources>
|
||||
|
|
|
@ -295,4 +295,7 @@
|
|||
<string name="you_have_to_be_connected_to_the_internet_in_order_to_add_an_account">You have to be connected to the internet in order to add an account.</string>
|
||||
<string name="simple_next">Next</string>
|
||||
<string name="simple_prev">Previous</string>
|
||||
<string name="simple_backup">Backup</string>
|
||||
<string name="simple_repair">Repair</string>
|
||||
<string name="backup_and_repair">We recommend to backup all unsynchronized notes and then try to repair the setup.</string>
|
||||
</resources>
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
v3.4.10
|
||||
|
||||
- Open trashbin of files app instead of Web UI (#238)
|
||||
- 🗑 Open trashbin of files app instead of Web UI (#238)
|
||||
- ✅ Make links in checkbox list items clickable
|
||||
- 🐞 NextcloudFilesAppAccountNotFoundException - Trying to provide backup & repair steps (#1256)
|
|
@ -57,6 +57,9 @@ dependencies {
|
|||
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
|
||||
|
||||
testImplementation 'androidx.test:core:1.3.0'
|
||||
testImplementation 'androidx.arch.core:core-testing:2.1.0'
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation 'org.mockito:mockito-core:3.11.1'
|
||||
testImplementation 'org.robolectric:robolectric:4.5.1'
|
||||
}
|
|
@ -1,22 +1,19 @@
|
|||
package it.niedermann.android.markdown.markwon.plugins;
|
||||
|
||||
import android.text.Spannable;
|
||||
import android.text.style.URLSpan;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.commonmark.node.Link;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedList;
|
||||
import java.util.function.Function;
|
||||
|
||||
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||
import io.noties.markwon.MarkwonPlugin;
|
||||
import io.noties.markwon.MarkwonSpansFactory;
|
||||
import io.noties.markwon.core.CoreProps;
|
||||
import it.niedermann.android.markdown.markwon.span.InterceptedURLSpan;
|
||||
|
||||
import static android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE;
|
||||
import static it.niedermann.android.markdown.MarkdownUtil.getContentAsSpannable;
|
||||
|
||||
public class LinkClickInterceptorPlugin extends AbstractMarkwonPlugin {
|
||||
|
||||
@NonNull
|
||||
|
@ -27,20 +24,9 @@ public class LinkClickInterceptorPlugin extends AbstractMarkwonPlugin {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void afterSetText(@NonNull TextView textView) {
|
||||
super.afterSetText(textView);
|
||||
if (onLinkClickCallbacks.size() > 0) {
|
||||
final Spannable spannable = getContentAsSpannable(textView);
|
||||
final URLSpan[] spans = spannable.getSpans(0, spannable.length(), URLSpan.class);
|
||||
|
||||
for (URLSpan originalSpan : spans) {
|
||||
final InterceptedURLSpan interceptedSpan = new InterceptedURLSpan(onLinkClickCallbacks, originalSpan.getURL());
|
||||
final int start = spannable.getSpanStart(originalSpan);
|
||||
final int end = spannable.getSpanEnd(originalSpan);
|
||||
spannable.removeSpan(originalSpan);
|
||||
spannable.setSpan(interceptedSpan, start, end, SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
}
|
||||
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
|
||||
super.configureSpansFactory(builder);
|
||||
builder.setFactory(Link.class, (configuration, props) -> new InterceptedURLSpan(onLinkClickCallbacks, CoreProps.LINK_DESTINATION.get(props)));
|
||||
}
|
||||
|
||||
public void registerOnLinkClickCallback(@NonNull Function<String, Boolean> callback) {
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
package it.niedermann.android.markdown.markwon.plugins;
|
||||
|
||||
import android.util.Log;
|
||||
import android.text.Spannable;
|
||||
import android.text.style.ClickableSpan;
|
||||
import android.util.Range;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import org.commonmark.node.AbstractVisitor;
|
||||
import org.commonmark.node.Block;
|
||||
|
@ -12,16 +16,23 @@ import org.commonmark.node.Paragraph;
|
|||
import org.commonmark.node.SoftLineBreak;
|
||||
import org.commonmark.node.Text;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||
import io.noties.markwon.MarkwonVisitor;
|
||||
import io.noties.markwon.SpanFactory;
|
||||
import io.noties.markwon.SpannableBuilder;
|
||||
import io.noties.markwon.SpannableBuilder.Span;
|
||||
import io.noties.markwon.ext.tasklist.TaskListItem;
|
||||
import io.noties.markwon.ext.tasklist.TaskListProps;
|
||||
import io.noties.markwon.ext.tasklist.TaskListSpan;
|
||||
import it.niedermann.android.markdown.MarkdownUtil;
|
||||
import it.niedermann.android.markdown.markwon.span.ToggleTaskListSpan;
|
||||
|
||||
/**
|
||||
|
@ -30,8 +41,6 @@ import it.niedermann.android.markdown.markwon.span.ToggleTaskListSpan;
|
|||
*/
|
||||
public class ToggleableTaskListPlugin extends AbstractMarkwonPlugin {
|
||||
|
||||
private static final String TAG = ToggleableTaskListPlugin.class.getSimpleName();
|
||||
|
||||
@NonNull
|
||||
private final AtomicBoolean enabled = new AtomicBoolean(true);
|
||||
@NonNull
|
||||
|
@ -45,6 +54,10 @@ public class ToggleableTaskListPlugin extends AbstractMarkwonPlugin {
|
|||
this.enabled.set(enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares {@link TaskListSpan}s and marks each one with a {@link ToggleMarkerSpan} in the first step.
|
||||
* The {@link ToggleMarkerSpan} are different from {@link TaskListSpan}s as they will stop on nested tasks instead of spanning the whole tasks including its subtasks.
|
||||
*/
|
||||
@Override
|
||||
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
|
||||
builder.on(TaskListItem.class, (visitor, node) -> {
|
||||
|
@ -56,7 +69,6 @@ public class ToggleableTaskListPlugin extends AbstractMarkwonPlugin {
|
|||
.get(TaskListItem.class);
|
||||
final Object spans = spanFactory == null ? null :
|
||||
spanFactory.getSpans(visitor.configuration(), visitor.renderProps());
|
||||
|
||||
if (spans != null) {
|
||||
final TaskListSpan taskListSpan;
|
||||
if (spans instanceof TaskListSpan[]) {
|
||||
|
@ -75,13 +87,12 @@ public class ToggleableTaskListPlugin extends AbstractMarkwonPlugin {
|
|||
if (content > 0 && taskListSpan != null) {
|
||||
// maybe additionally identify this task list (for persistence)
|
||||
visitor.builder().setSpan(
|
||||
new ToggleTaskListSpan(enabled, toggleListener, taskListSpan, visitor.builder().subSequence(length, length + content).toString()),
|
||||
new ToggleMarkerSpan(taskListSpan),
|
||||
length,
|
||||
length + content
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SpannableBuilder.setSpans(
|
||||
visitor.builder(),
|
||||
spans,
|
||||
|
@ -95,7 +106,86 @@ public class ToggleableTaskListPlugin extends AbstractMarkwonPlugin {
|
|||
});
|
||||
}
|
||||
|
||||
static class TaskListContextVisitor extends AbstractVisitor {
|
||||
|
||||
/**
|
||||
* Adds for each symbolic {@link ToggleMarkerSpan} an actual {@link ToggleTaskListSpan}s respecting existing {@link ClickableSpan}s.
|
||||
*/
|
||||
@Override
|
||||
public void afterRender(@NonNull Node node, @NonNull MarkwonVisitor visitor) {
|
||||
super.afterRender(node, visitor);
|
||||
|
||||
final List<Span> markerSpans = getSortedSpans(visitor.builder(), ToggleMarkerSpan.class, 0, visitor.builder().length());
|
||||
|
||||
for (int position = 0; position < markerSpans.size(); position++) {
|
||||
final Span markerSpan = markerSpans.get(position);
|
||||
final int start = markerSpan.start;
|
||||
final int end = markerSpan.end;
|
||||
final Collection<Range<Integer>> freeRanges = findFreeRanges(visitor.builder(), start, end);
|
||||
for (Range<Integer> freeRange : freeRanges) {
|
||||
visitor.builder().setSpan(
|
||||
new ToggleTaskListSpan(enabled, toggleListener, ((ToggleMarkerSpan) markerSpan.what).getTaskListSpan(), position),
|
||||
freeRange.getLower(), freeRange.getUpper());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes {@link ToggleMarkerSpan}s from {@param textView}.
|
||||
*/
|
||||
@Override
|
||||
public void afterSetText(@NonNull TextView textView) {
|
||||
super.afterSetText(textView);
|
||||
final Spannable spannable = MarkdownUtil.getContentAsSpannable(textView);
|
||||
for (ToggleMarkerSpan span : spannable.getSpans(0, spannable.length(), ToggleMarkerSpan.class)) {
|
||||
spannable.removeSpan(span);
|
||||
}
|
||||
textView.setText(spannable);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a {@link List} of {@link Range}s in the given {@param spanned} from {@param start} to {@param end} which is <strong>not</strong> taken for a {@link ClickableSpan}.
|
||||
*/
|
||||
@NonNull
|
||||
private static Collection<Range<Integer>> findFreeRanges(@NonNull SpannableBuilder builder, int start, int end) {
|
||||
final List<Range<Integer>> freeRanges;
|
||||
final List<Span> clickableSpans = getSortedSpans(builder, ClickableSpan.class, start, end);
|
||||
if (clickableSpans.size() > 0) {
|
||||
freeRanges = new LinkedList<>();
|
||||
int from = start;
|
||||
for (Span clickableSpan : clickableSpans) {
|
||||
final int clickableStart = clickableSpan.start;
|
||||
final int clickableEnd = clickableSpan.end;
|
||||
if (from != clickableStart) {
|
||||
freeRanges.add(new Range<>(from, clickableStart));
|
||||
}
|
||||
from = clickableEnd;
|
||||
}
|
||||
if (clickableSpans.size() > 0) {
|
||||
final int lastUpperBlocker = clickableSpans.get(clickableSpans.size() - 1).end;
|
||||
if (lastUpperBlocker < end) {
|
||||
freeRanges.add(new Range<>(lastUpperBlocker, end));
|
||||
}
|
||||
}
|
||||
} else if (start == end) {
|
||||
freeRanges = Collections.emptyList();
|
||||
} else {
|
||||
freeRanges = Collections.singletonList(new Range<>(start, end));
|
||||
}
|
||||
return freeRanges;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a {@link List} of {@link Span}s holding {@param type}s, sorted ascending by the span start.
|
||||
*/
|
||||
private static <T> List<Span> getSortedSpans(@NonNull SpannableBuilder builder, @NonNull Class<T> type, int start, int end) {
|
||||
return builder.getSpans(start, end)
|
||||
.stream()
|
||||
.filter(span -> type.isInstance(span.what))
|
||||
.sorted((o1, o2) -> o1.start - o2.start)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private static final class TaskListContextVisitor extends AbstractVisitor {
|
||||
private int contentLength = 0;
|
||||
|
||||
static int contentLength(Node node) {
|
||||
|
@ -139,4 +229,23 @@ public class ToggleableTaskListPlugin extends AbstractMarkwonPlugin {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class which holds an {@link TaskListSpan} but does not include the range of child {@link TaskListSpan}s.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static final class ToggleMarkerSpan {
|
||||
|
||||
@NonNull
|
||||
private final TaskListSpan taskListSpan;
|
||||
|
||||
private ToggleMarkerSpan(@NonNull TaskListSpan taskListSpan) {
|
||||
this.taskListSpan = taskListSpan;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private TaskListSpan getTaskListSpan() {
|
||||
return taskListSpan;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
package it.niedermann.android.markdown.markwon.span;
|
||||
|
||||
import android.text.Spanned;
|
||||
import android.text.style.URLSpan;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
|
@ -22,7 +24,6 @@ public class InterceptedURLSpan extends URLSpan {
|
|||
super(url);
|
||||
this.onLinkClickCallbacks = onLinkClickCallbacks;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View widget) {
|
||||
if (onLinkClickCallbacks.size() > 0) {
|
||||
|
|
|
@ -1,15 +1,12 @@
|
|||
package it.niedermann.android.markdown.markwon.span;
|
||||
|
||||
import android.text.Spanned;
|
||||
import android.text.TextPaint;
|
||||
import android.text.style.ClickableSpan;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
|
@ -19,42 +16,24 @@ public class ToggleTaskListSpan extends ClickableSpan {
|
|||
|
||||
private static final String TAG = ToggleTaskListSpan.class.getSimpleName();
|
||||
|
||||
final AtomicBoolean enabled;
|
||||
final BiConsumer<Integer, Boolean> toggleListener;
|
||||
final TaskListSpan span;
|
||||
final String content;
|
||||
private final AtomicBoolean enabled;
|
||||
private final BiConsumer<Integer, Boolean> toggleListener;
|
||||
private final TaskListSpan span;
|
||||
private final int position;
|
||||
|
||||
public ToggleTaskListSpan(@NonNull AtomicBoolean enabled, @NonNull BiConsumer<Integer, Boolean> toggleListener, @NonNull TaskListSpan span, String content) {
|
||||
public ToggleTaskListSpan(@NonNull AtomicBoolean enabled, @NonNull BiConsumer<Integer, Boolean> toggleListener, @NonNull TaskListSpan span, int position) {
|
||||
this.enabled = enabled;
|
||||
this.toggleListener = toggleListener;
|
||||
this.span = span;
|
||||
this.content = content;
|
||||
this.position = position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(@NonNull View widget) {
|
||||
if(enabled.get()) {
|
||||
if (enabled.get()) {
|
||||
span.setDone(!span.isDone());
|
||||
widget.invalidate();
|
||||
|
||||
// it must be a TextView
|
||||
final TextView textView = (TextView) widget;
|
||||
// it must be spanned
|
||||
// TODO what if textView is not a spanned?
|
||||
final Spanned spanned = (Spanned) textView.getText();
|
||||
|
||||
final ClickableSpan[] toggles = spanned.getSpans(0, spanned.length(), getClass());
|
||||
Arrays.sort(toggles, (o1, o2) -> spanned.getSpanStart(o1) - spanned.getSpanStart(o2));
|
||||
|
||||
int currentTogglePosition = -1;
|
||||
for (int i = 0; i < toggles.length; i++) {
|
||||
if (spanned.getSpanStart(toggles[i]) == spanned.getSpanStart(this) && spanned.getSpanEnd(toggles[i]) == spanned.getSpanEnd(this)) {
|
||||
currentTogglePosition = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
toggleListener.accept(currentTogglePosition, span.isDone());
|
||||
toggleListener.accept(position, span.isDone());
|
||||
} else {
|
||||
Log.w(TAG, "Prevented toggling checkbox because the view is disabled");
|
||||
}
|
||||
|
|
|
@ -0,0 +1,204 @@
|
|||
package it.niedermann.android.markdown.markwon.plugins;
|
||||
|
||||
import android.text.Editable;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.style.ClickableSpan;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.text.style.URLSpan;
|
||||
import android.util.Range;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import org.commonmark.node.Node;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import io.noties.markwon.MarkwonVisitor;
|
||||
import io.noties.markwon.SpannableBuilder;
|
||||
import io.noties.markwon.ext.tasklist.TaskListSpan;
|
||||
import it.niedermann.android.markdown.markwon.span.InterceptedURLSpan;
|
||||
import it.niedermann.android.markdown.markwon.span.ToggleTaskListSpan;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public class ToggleableTaskListPluginTest extends TestCase {
|
||||
|
||||
@Test
|
||||
public void testAfterRender() throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
|
||||
final Node node = mock(Node.class);
|
||||
final MarkwonVisitor visitor = mock(MarkwonVisitor.class);
|
||||
|
||||
final Constructor<ToggleableTaskListPlugin.ToggleMarkerSpan> markerSpanConstructor = ToggleableTaskListPlugin.ToggleMarkerSpan.class.getDeclaredConstructor(TaskListSpan.class);
|
||||
markerSpanConstructor.setAccessible(true);
|
||||
|
||||
final SpannableBuilder builder = new SpannableBuilder("Lorem Ipsum Dolor \nSit Amet");
|
||||
builder.setSpan(markerSpanConstructor.newInstance(mock(TaskListSpan.class)), 0, 6);
|
||||
builder.setSpan(new URLSpan(""), 6, 11);
|
||||
builder.setSpan(markerSpanConstructor.newInstance(mock(TaskListSpan.class)), 11, 19);
|
||||
builder.setSpan(new InterceptedURLSpan(Collections.emptyList(), ""), 19, 22);
|
||||
builder.setSpan(markerSpanConstructor.newInstance(mock(TaskListSpan.class)), 22, 27);
|
||||
|
||||
when(visitor.builder()).thenReturn(builder);
|
||||
|
||||
final ToggleableTaskListPlugin plugin = new ToggleableTaskListPlugin((i, b) -> {
|
||||
// Do nothing...
|
||||
});
|
||||
plugin.afterRender(node, visitor);
|
||||
|
||||
// We ignore marker spans in this test. They will be removed in another step
|
||||
final List<SpannableBuilder.Span> spans = builder.getSpans(0, builder.length())
|
||||
.stream()
|
||||
.filter(span -> span.what.getClass() != ToggleableTaskListPlugin.ToggleMarkerSpan.class)
|
||||
.sorted((o1, o2) -> o1.start - o2.start)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
assertEquals(5, spans.size());
|
||||
assertEquals(ToggleTaskListSpan.class, spans.get(0).what.getClass());
|
||||
assertEquals(0, spans.get(0).start);
|
||||
assertEquals(6, spans.get(0).end);
|
||||
assertEquals(URLSpan.class, spans.get(1).what.getClass());
|
||||
assertEquals(6, spans.get(1).start);
|
||||
assertEquals(11, spans.get(1).end);
|
||||
assertEquals(ToggleTaskListSpan.class, spans.get(2).what.getClass());
|
||||
assertEquals(11, spans.get(2).start);
|
||||
assertEquals(19, spans.get(2).end);
|
||||
assertEquals(InterceptedURLSpan.class, spans.get(3).what.getClass());
|
||||
assertEquals(19, spans.get(3).start);
|
||||
assertEquals(22, spans.get(3).end);
|
||||
assertEquals(ToggleTaskListSpan.class, spans.get(4).what.getClass());
|
||||
assertEquals(22, spans.get(4).start);
|
||||
assertEquals(27, spans.get(4).end);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAfterSetText() throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
|
||||
final Constructor<ToggleableTaskListPlugin.ToggleMarkerSpan> markerSpanConstructor = ToggleableTaskListPlugin.ToggleMarkerSpan.class.getDeclaredConstructor(TaskListSpan.class);
|
||||
markerSpanConstructor.setAccessible(true);
|
||||
|
||||
final Editable editable = new SpannableStringBuilder("Lorem Ipsum Dolor \nSit Amet");
|
||||
editable.setSpan(markerSpanConstructor.newInstance(mock(TaskListSpan.class)), 0, 6, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
editable.setSpan(new URLSpan(""), 6, 11, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
editable.setSpan(markerSpanConstructor.newInstance(mock(TaskListSpan.class)), 11, 19, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
editable.setSpan(new InterceptedURLSpan(Collections.emptyList(), ""), 19, 22, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
editable.setSpan(markerSpanConstructor.newInstance(mock(TaskListSpan.class)), 22, 27, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
final TextView textView = new TextView(ApplicationProvider.getApplicationContext());
|
||||
textView.setText(editable);
|
||||
|
||||
assertEquals(3, ((Spanned) textView.getText()).getSpans(0, textView.getText().length(), ToggleableTaskListPlugin.ToggleMarkerSpan.class).length);
|
||||
|
||||
final ToggleableTaskListPlugin plugin = new ToggleableTaskListPlugin((i, b) -> {
|
||||
// Do nothing...
|
||||
});
|
||||
plugin.afterSetText(textView);
|
||||
|
||||
assertEquals(0, ((Spanned) textView.getText()).getSpans(0, textView.getText().length(), ToggleableTaskListPlugin.ToggleMarkerSpan.class).length);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({"unchecked", "ConstantConditions"})
|
||||
public void testGetSortedSpans() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
|
||||
final Method m = ToggleableTaskListPlugin.class.getDeclaredMethod("getSortedSpans", SpannableBuilder.class, Class.class, int.class, int.class);
|
||||
m.setAccessible(true);
|
||||
|
||||
final Object firstClickableSpan = new URLSpan("");
|
||||
final Object secondClickableSpan = new InterceptedURLSpan(Collections.emptyList(), "");
|
||||
final Object unclickableSpan = new ForegroundColorSpan(android.R.color.white);
|
||||
|
||||
final SpannableBuilder spannable = new SpannableBuilder("Lorem Ipsum Dolor \nSit Amet");
|
||||
spannable.setSpan(firstClickableSpan, 6, 11, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
spannable.setSpan(secondClickableSpan, 19, 22, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
spannable.setSpan(unclickableSpan, 3, 20, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
List<SpannableBuilder.Span> clickableSpans;
|
||||
|
||||
clickableSpans = (List<SpannableBuilder.Span>) m.invoke(null, spannable, ClickableSpan.class, 0, 0);
|
||||
assertEquals(0, clickableSpans.size());
|
||||
|
||||
clickableSpans = (List<SpannableBuilder.Span>) m.invoke(null, spannable, ClickableSpan.class, spannable.length() - 1, spannable.length() - 1);
|
||||
assertEquals(0, clickableSpans.size());
|
||||
|
||||
clickableSpans = (List<SpannableBuilder.Span>) m.invoke(null, spannable, ClickableSpan.class, 0, 5);
|
||||
assertEquals(0, clickableSpans.size());
|
||||
|
||||
clickableSpans = (List<SpannableBuilder.Span>) m.invoke(null, spannable, ClickableSpan.class, 0, spannable.length());
|
||||
assertEquals(2, clickableSpans.size());
|
||||
assertEquals(firstClickableSpan, clickableSpans.get(0).what);
|
||||
assertEquals(secondClickableSpan, clickableSpans.get(1).what);
|
||||
|
||||
clickableSpans = (List<SpannableBuilder.Span>) m.invoke(null, spannable, ClickableSpan.class, 0, 17);
|
||||
assertEquals(1, clickableSpans.size());
|
||||
assertEquals(firstClickableSpan, clickableSpans.get(0).what);
|
||||
|
||||
clickableSpans = (List<SpannableBuilder.Span>) m.invoke(null, spannable, ClickableSpan.class, 12, 22);
|
||||
assertEquals(1, clickableSpans.size());
|
||||
assertEquals(secondClickableSpan, clickableSpans.get(0).what);
|
||||
|
||||
clickableSpans = (List<SpannableBuilder.Span>) m.invoke(null, spannable, ClickableSpan.class, 9, 20);
|
||||
assertEquals(2, clickableSpans.size());
|
||||
assertEquals(firstClickableSpan, clickableSpans.get(0).what);
|
||||
assertEquals(secondClickableSpan, clickableSpans.get(1).what);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({"unchecked", "ConstantConditions"})
|
||||
public void testFindFreeRanges() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
|
||||
final Method m = ToggleableTaskListPlugin.class.getDeclaredMethod("findFreeRanges", SpannableBuilder.class, int.class, int.class);
|
||||
m.setAccessible(true);
|
||||
|
||||
final Object firstClickableSpan = new URLSpan("");
|
||||
final Object secondClickableSpan = new InterceptedURLSpan(Collections.emptyList(), "");
|
||||
final SpannableBuilder spannable = new SpannableBuilder("Lorem Ipsum Dolor \nSit Amet");
|
||||
spannable.setSpan(firstClickableSpan, 6, 11, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
spannable.setSpan(secondClickableSpan, 19, 22, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
List<Range<Integer>> freeRanges;
|
||||
|
||||
freeRanges = (List<Range<Integer>>) m.invoke(null, spannable, 0, 0);
|
||||
assertEquals(0, freeRanges.size());
|
||||
|
||||
freeRanges = (List<Range<Integer>>) m.invoke(null, spannable, spannable.length() - 1, spannable.length() - 1);
|
||||
assertEquals(0, freeRanges.size());
|
||||
|
||||
freeRanges = (List<Range<Integer>>) m.invoke(null, spannable, 0, 6);
|
||||
assertEquals(1, freeRanges.size());
|
||||
assertEquals(0, (int) freeRanges.get(0).getLower());
|
||||
assertEquals(6, (int) freeRanges.get(0).getUpper());
|
||||
|
||||
freeRanges = (List<Range<Integer>>) m.invoke(null, spannable, 0, 6);
|
||||
assertEquals(1, freeRanges.size());
|
||||
assertEquals(0, (int) freeRanges.get(0).getLower());
|
||||
assertEquals(6, (int) freeRanges.get(0).getUpper());
|
||||
|
||||
freeRanges = (List<Range<Integer>>) m.invoke(null, spannable, 3, 15);
|
||||
assertEquals(2, freeRanges.size());
|
||||
assertEquals(3, (int) freeRanges.get(0).getLower());
|
||||
assertEquals(6, (int) freeRanges.get(0).getUpper());
|
||||
assertEquals(11, (int) freeRanges.get(1).getLower());
|
||||
assertEquals(15, (int) freeRanges.get(1).getUpper());
|
||||
|
||||
freeRanges = (List<Range<Integer>>) m.invoke(null, spannable, 0, spannable.length());
|
||||
assertEquals(3, freeRanges.size());
|
||||
assertEquals(0, (int) freeRanges.get(0).getLower());
|
||||
assertEquals(6, (int) freeRanges.get(0).getUpper());
|
||||
assertEquals(11, (int) freeRanges.get(1).getLower());
|
||||
assertEquals(19, (int) freeRanges.get(1).getUpper());
|
||||
assertEquals(22, (int) freeRanges.get(2).getLower());
|
||||
assertEquals(27, (int) freeRanges.get(2).getUpper());
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue