Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/stefan-niedermann/nextcloud-deck.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authordesperateCoder <echotodevnull@gmail.com>2020-11-14 15:50:47 +0300
committerdesperateCoder <echotodevnull@gmail.com>2020-11-14 15:50:47 +0300
commitd235ae16f1e582fc21a8631ccb3ecd840fba13b4 (patch)
treeef13a3464a6beab9b74499a7d865b448210c7b87 /app
parent58903257509071f9d245132697d20ace6be17600 (diff)
parent0f8b8d55fa6c5de2072e306ef3446dab11f155eb (diff)
Merge branch 'master' of github.com:stefan-niedermann/nextcloud-deck
Diffstat (limited to 'app')
-rw-r--r--app/build.gradle15
-rw-r--r--app/src/main/AndroidManifest.xml11
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/ImportAccountActivity.java1
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditActivity.java28
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/assignee/CardAssigneeDialog.java9
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentAdapter.java31
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentsBottomsheetBehaviorCallback.java91
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentsFragment.java452
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/DefaultAttachmentViewHolder.java24
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/AbstractCursorPickerAdapter.java100
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/AbstractPickerAdapter.java26
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/ContactAdapter.java104
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/ContactItemViewHolder.java66
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/ContactNativeItemViewHolder.java23
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileAdapter.java85
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileAdapterLegacy.java88
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileItemViewHolder.java45
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileNativeItemViewHolder.java23
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryAdapter.java100
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryItemDecoration.java29
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryItemViewHolder.java42
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryPhotoPreviewItemViewHolder.java51
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/previewdialog/PreviewDialog.java102
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/previewdialog/PreviewDialogViewModel.java50
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserAdapter.java4
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/takephoto/TakePhotoActivity.java182
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/takephoto/TakePhotoViewModel.java57
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/view/SquareConstraintLayout.java35
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/util/AttachmentUtil.java79
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/util/MimeTypeUtil.java1
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/util/VCardUtil.java42
-rw-r--r--app/src/main/res/drawable-v21/bottom_sheet_rounded.xml9
-rw-r--r--app/src/main/res/drawable-xxxhdpi/background.pngbin44461 -> 0 bytes
-rw-r--r--app/src/main/res/drawable/bottom_sheet_rounded.xml8
-rw-r--r--app/src/main/res/drawable/ic_baseline_camera_front_24.xml5
-rw-r--r--app/src/main/res/drawable/ic_baseline_camera_rear_24.xml5
-rw-r--r--app/src/main/res/drawable/ic_baseline_flash_off_24.xml5
-rw-r--r--app/src/main/res/drawable/ic_baseline_flash_on_24.xml5
-rw-r--r--app/src/main/res/drawable/ic_baseline_photo_camera_24.xml6
-rw-r--r--app/src/main/res/drawable/ic_baseline_search_24.xml5
-rw-r--r--app/src/main/res/layout/activity_take_photo.xml55
-rw-r--r--app/src/main/res/layout/dialog_preview.xml (renamed from app/src/main/res/layout/dialog_assignee.xml)2
-rw-r--r--app/src/main/res/layout/fragment_card_edit_tab_attachments.xml59
-rw-r--r--app/src/main/res/layout/item_attachment_default.xml19
-rw-r--r--app/src/main/res/layout/item_filter_user.xml2
-rw-r--r--app/src/main/res/layout/item_photo_preview.xml27
-rw-r--r--app/src/main/res/layout/item_picker_native.xml51
-rw-r--r--app/src/main/res/layout/item_picker_user.xml59
-rw-r--r--app/src/main/res/menu/attachment_picker_menu.xml19
-rw-r--r--app/src/main/res/values-ca/strings.xml2
-rw-r--r--app/src/main/res/values-cs-rCZ/strings.xml9
-rw-r--r--app/src/main/res/values-de/strings.xml22
-rw-r--r--app/src/main/res/values-el/strings.xml2
-rw-r--r--app/src/main/res/values-es/strings.xml20
-rw-r--r--app/src/main/res/values-fr/strings.xml17
-rw-r--r--app/src/main/res/values-gl/strings.xml16
-rw-r--r--app/src/main/res/values-hu-rHU/strings.xml2
-rw-r--r--app/src/main/res/values-it/strings.xml16
-rw-r--r--app/src/main/res/values-pl/strings.xml16
-rw-r--r--app/src/main/res/values-pt-rBR/strings.xml9
-rw-r--r--app/src/main/res/values-sl/strings.xml9
-rw-r--r--app/src/main/res/values-sr/strings.xml2
-rw-r--r--app/src/main/res/values-sv/strings.xml46
-rw-r--r--app/src/main/res/values-tr/strings.xml16
-rw-r--r--app/src/main/res/values-v21/styles.xml9
-rw-r--r--app/src/main/res/values-zh-rCN/strings.xml10
-rw-r--r--app/src/main/res/values/dimens.xml1
-rw-r--r--app/src/main/res/values/strings.xml16
-rw-r--r--app/src/main/res/values/styles.xml14
69 files changed, 2393 insertions, 198 deletions
diff --git a/app/build.gradle b/app/build.gradle
index 3036fcdbe..99814978e 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,13 +1,13 @@
apply plugin: 'com.android.application'
android {
- compileSdkVersion 30
+ compileSdkVersion 29
defaultConfig {
applicationId "it.niedermann.nextcloud.deck"
minSdkVersion 19
- targetSdkVersion 30
- versionCode 1011001
- versionName "1.11.1"
+ targetSdkVersion 29
+ versionCode 1012001
+ versionName "1.12.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
javaCompileOptions {
@@ -67,6 +67,10 @@ dependencies {
implementation 'com.github.bumptech.glide:glide:4.11.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
+ implementation "androidx.camera:camera-camera2:1.0.0-beta12"
+ implementation "androidx.camera:camera-lifecycle:1.0.0-beta12"
+ implementation "androidx.camera:camera-view:1.0.0-alpha19"
+
// Markdown
implementation 'com.yydcdut:markdown-processor:0.1.3'
implementation 'com.yydcdut:rxmarkdown-wrapper:0.1.3'
@@ -75,6 +79,7 @@ dependencies {
// Android X
implementation 'com.google.android.material:material:1.2.1'
implementation 'androidx.appcompat:appcompat:1.2.0'
+ implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation "androidx.viewpager2:viewpager2:1.0.0"
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.work:work-runtime:2.4.0'
@@ -118,7 +123,7 @@ dependencies {
// -----------------------
implementation 'androidx.multidex:multidex:2.0.1'
- coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.10'
+ coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1'
// -------------
// --- Tests ---
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index d06f329f2..f936fac67 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -6,6 +6,12 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+ <uses-permission android:name="android.permission.READ_CONTACTS" />
+ <uses-permission android:name="android.permission.CAMERA" />
+
+ <uses-feature android:name="android.hardware.camera.any" />
+
+ <uses-sdk tools:overrideLibrary="androidx.camera.core, androidx.camera.camera2, androidx.camera.lifecycle, androidx.camera.view" />
<application
android:name="it.niedermann.nextcloud.deck.DeckApplication"
@@ -50,6 +56,11 @@
android:windowSoftInputMode="stateHidden" />
<activity
+ android:name=".ui.takephoto.TakePhotoActivity"
+ android:theme="@style/TakePhotoTheme"
+ android:windowSoftInputMode="stateHidden" />
+
+ <activity
android:name=".ui.sharetarget.ShareTargetActivity"
android:label="@string/share_add_to_card"
android:theme="@style/SplashTheme">
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/ImportAccountActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/ImportAccountActivity.java
index 957743919..3ded31bb7 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/ImportAccountActivity.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/ImportAccountActivity.java
@@ -83,6 +83,7 @@ public class ImportAccountActivity extends AppCompatActivity {
DeckLog.warn("=============================================================");
DeckLog.warn("Nextcloud app is not installed. Cannot choose account");
DeckLog.logError(e);
+ binding.addButton.setEnabled(true);
} catch (AndroidGetAccountsPermissionNotGranted e) {
binding.addButton.setEnabled(true);
AccountImporter.requestAndroidAccountPermissionsAndPickAccount(this);
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditActivity.java
index 1af96bca5..fcc9d24e6 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditActivity.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditActivity.java
@@ -25,6 +25,7 @@ import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.ActivityEditBinding;
import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.deck.model.full.FullCard;
import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
import it.niedermann.nextcloud.deck.ui.branding.BrandedActivity;
import it.niedermann.nextcloud.deck.ui.branding.BrandedAlertDialogBuilder;
@@ -171,12 +172,16 @@ public class EditActivity extends BrandedActivity {
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.action_card_save) {
- saveAndFinish();
+ saveAndRun(super::finish);
}
return super.onOptionsItemSelected(item);
}
- private void saveAndFinish() {
+ /**
+ * Tries to save the current {@link FullCard} from the {@link EditCardViewModel} and then runs the given {@link Runnable}
+ * @param runnable
+ */
+ private void saveAndRun(@NonNull Runnable runnable) {
if (!viewModel.isPendingCreation()) {
viewModel.setPendingCreation(true);
final String title = viewModel.getFullCard().getCard().getTitle();
@@ -194,9 +199,9 @@ public class EditActivity extends BrandedActivity {
.show();
} else {
if (viewModel.isCreateMode()) {
- observeOnce(syncManager.createFullCard(viewModel.getAccount().getId(), viewModel.getBoardId(), viewModel.getFullCard().getCard().getStackId(), viewModel.getFullCard()), EditActivity.this, (card) -> super.finish());
+ observeOnce(syncManager.createFullCard(viewModel.getAccount().getId(), viewModel.getBoardId(), viewModel.getFullCard().getCard().getStackId(), viewModel.getFullCard()), EditActivity.this, (card) -> runnable.run());
} else {
- observeOnce(syncManager.updateCard(viewModel.getFullCard()), EditActivity.this, (card) -> super.finish());
+ observeOnce(syncManager.updateCard(viewModel.getFullCard()), EditActivity.this, (card) -> runnable.run());
}
}
}
@@ -265,26 +270,15 @@ public class EditActivity extends BrandedActivity {
}
@Override
- public boolean onSupportNavigateUp() {
- finish(); // close this activity as oppose to navigating up
- return true;
- }
-
- @Override
- public void onBackPressed() {
- finish();
- }
-
- @Override
public void finish() {
if (!viewModel.hasChanges() && viewModel.canEdit()) {
new BrandedAlertDialogBuilder(this)
.setTitle(R.string.simple_save)
.setMessage(R.string.do_you_want_to_save_your_changes)
- .setPositiveButton(R.string.simple_save, (dialog, whichButton) -> saveAndFinish())
+ .setPositiveButton(R.string.simple_save, (dialog, whichButton) -> saveAndRun(super::finish))
.setNegativeButton(R.string.simple_discard, (dialog, whichButton) -> super.finish()).show();
} else {
- directFinish();
+ super.finish();
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/assignee/CardAssigneeDialog.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/assignee/CardAssigneeDialog.java
index 74c89d830..34d2eb3f3 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/assignee/CardAssigneeDialog.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/assignee/CardAssigneeDialog.java
@@ -19,7 +19,7 @@ import com.bumptech.glide.Glide;
import java.io.Serializable;
import it.niedermann.nextcloud.deck.R;
-import it.niedermann.nextcloud.deck.databinding.DialogAssigneeBinding;
+import it.niedermann.nextcloud.deck.databinding.DialogPreviewBinding;
import it.niedermann.nextcloud.deck.model.User;
import it.niedermann.nextcloud.deck.ui.branding.BrandedDeleteAlertDialogBuilder;
import it.niedermann.nextcloud.deck.ui.branding.BrandedDialogFragment;
@@ -27,10 +27,11 @@ import it.niedermann.nextcloud.deck.ui.card.EditCardViewModel;
import static it.niedermann.nextcloud.deck.DeckApplication.isDarkTheme;
+@Deprecated
public class CardAssigneeDialog extends BrandedDialogFragment {
private static final String KEY_USER = "user";
- private DialogAssigneeBinding binding;
+ private DialogPreviewBinding binding;
private EditCardViewModel viewModel;
@Nullable
@@ -63,7 +64,7 @@ public class CardAssigneeDialog extends BrandedDialogFragment {
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
- binding = DialogAssigneeBinding.inflate(LayoutInflater.from(requireContext()));
+ binding = DialogPreviewBinding.inflate(LayoutInflater.from(requireContext()));
viewModel = new ViewModelProvider(requireActivity()).get(EditCardViewModel.class);
AlertDialog.Builder dialogBuilder = new BrandedDeleteAlertDialogBuilder(requireContext());
@@ -95,7 +96,7 @@ public class CardAssigneeDialog extends BrandedDialogFragment {
.placeholder(circularProgressDrawable)
.error(R.drawable.ic_person_grey600_24dp)
.into(binding.avatar));
- binding.displayName.setText(user.getDisplayname());
+ binding.title.setText(user.getDisplayname());
}
@Override
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentAdapter.java
index ea347417a..d601f6bbd 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentAdapter.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentAdapter.java
@@ -14,6 +14,8 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.ActivityOptionsCompat;
import androidx.fragment.app.FragmentManager;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
@@ -28,6 +30,7 @@ import it.niedermann.nextcloud.deck.ui.attachments.AttachmentsActivity;
import it.niedermann.nextcloud.deck.ui.branding.Branded;
import it.niedermann.nextcloud.deck.util.MimeTypeUtil;
+import static androidx.lifecycle.Transformations.distinctUntilChanged;
import static androidx.recyclerview.widget.RecyclerView.NO_ID;
import static it.niedermann.nextcloud.deck.util.AttachmentUtil.openAttachmentInBrowser;
@@ -37,6 +40,9 @@ public class CardAttachmentAdapter extends RecyclerView.Adapter<AttachmentViewHo
public static final int VIEW_TYPE_DEFAULT = 2;
public static final int VIEW_TYPE_IMAGE = 1;
+ @NonNull
+ private final MutableLiveData<Boolean> isEmpty = new MutableLiveData<>(true);
+ @NonNull
private final MenuInflater menuInflater;
@ColorInt
private int mainColor;
@@ -45,9 +51,9 @@ public class CardAttachmentAdapter extends RecyclerView.Adapter<AttachmentViewHo
private Long cardRemoteId = null;
private final long cardLocalId;
@NonNull
- FragmentManager fragmentManager;
+ private final FragmentManager fragmentManager;
@NonNull
- private List<Attachment> attachments = new ArrayList<>();
+ private final List<Attachment> attachments = new ArrayList<>();
@NonNull
private final AttachmentClickedListener attachmentClickedListener;
@@ -126,22 +132,41 @@ public class CardAttachmentAdapter extends RecyclerView.Adapter<AttachmentViewHo
return attachments.size();
}
+ private void updateIsEmpty() {
+ this.isEmpty.postValue(getItemCount() <= 0);
+ }
+
+ @NonNull
+ public LiveData<Boolean> isEmpty() {
+ return distinctUntilChanged(this.isEmpty);
+ }
+
public void setAttachments(@NonNull List<Attachment> attachments, @Nullable Long cardRemoteId) {
this.cardRemoteId = cardRemoteId;
this.attachments.clear();
this.attachments.addAll(attachments);
notifyDataSetChanged();
+ this.updateIsEmpty();
}
public void addAttachment(Attachment a) {
- this.attachments.add(a);
+ this.attachments.add(0, a);
notifyItemInserted(this.attachments.size());
+ this.updateIsEmpty();
}
public void removeAttachment(Attachment a) {
final int index = this.attachments.indexOf(a);
this.attachments.remove(a);
notifyItemRemoved(index);
+ this.updateIsEmpty();
+ }
+
+ public void replaceAttachment(Attachment toReplace, Attachment with) {
+ final int index = this.attachments.indexOf(toReplace);
+ this.attachments.remove(toReplace);
+ this.attachments.add(index, with);
+ notifyItemChanged(index);
}
@Override
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentsBottomsheetBehaviorCallback.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentsBottomsheetBehaviorCallback.java
new file mode 100644
index 000000000..6b60bbffd
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentsBottomsheetBehaviorCallback.java
@@ -0,0 +1,91 @@
+package it.niedermann.nextcloud.deck.ui.card.attachments;
+
+import android.content.Context;
+import android.view.View;
+
+import androidx.activity.OnBackPressedCallback;
+import androidx.annotation.ColorInt;
+import androidx.annotation.ColorRes;
+import androidx.annotation.DimenRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Px;
+import androidx.core.content.ContextCompat;
+
+import com.google.android.material.animation.ArgbEvaluatorCompat;
+import com.google.android.material.bottomnavigation.BottomNavigationView;
+import com.google.android.material.bottomsheet.BottomSheetBehavior;
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+
+import it.niedermann.android.util.DimensionUtil;
+
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HIDDEN;
+
+public class CardAttachmentsBottomsheetBehaviorCallback extends BottomSheetBehavior.BottomSheetCallback {
+ @NonNull
+ private final OnBackPressedCallback backPressedCallback;
+ @NonNull
+ private final FloatingActionButton fab;
+ @NonNull
+ private final View pickerBackdrop;
+ @NonNull
+ private final BottomNavigationView bottomNavigation;
+ @ColorInt
+ private final int backdropColorExpanded;
+ @ColorInt
+ private final int backdropColorCollapsed;
+ @Px
+ private final int bottomNavigationHeight;
+
+ private float lastOffset = -1;
+
+ public CardAttachmentsBottomsheetBehaviorCallback(@NonNull Context context,
+ @NonNull OnBackPressedCallback backPressedCallback,
+ @NonNull FloatingActionButton fab,
+ @NonNull View pickerBackdrop,
+ @NonNull BottomNavigationView bottomNavigation,
+ @ColorRes int backdropColorExpanded,
+ @ColorRes int backdropColorCollapsed,
+ @DimenRes int bottomNavigationHeight
+ ) {
+ this.backPressedCallback = backPressedCallback;
+ this.fab = fab;
+ this.pickerBackdrop = pickerBackdrop;
+ this.bottomNavigation = bottomNavigation;
+ this.backdropColorExpanded = ContextCompat.getColor(context, backdropColorExpanded);
+ this.backdropColorCollapsed = ContextCompat.getColor(context, backdropColorCollapsed);
+ this.bottomNavigationHeight = DimensionUtil.INSTANCE.dpToPx(context, bottomNavigationHeight);
+ }
+
+ @Override
+ public void onStateChanged(@NonNull View bottomSheet, int newState) {
+ if (newState == STATE_HIDDEN) {
+ backPressedCallback.setEnabled(false);
+ if (pickerBackdrop.getVisibility() != GONE) {
+ pickerBackdrop.setVisibility(GONE);
+ }
+ } else if (pickerBackdrop.getVisibility() != VISIBLE) {
+ pickerBackdrop.setVisibility(VISIBLE);
+ }
+ }
+
+ @Override
+ public void onSlide(@NonNull View bottomSheet, float slideOffset) {
+ if (slideOffset <= 0) {
+ final float bottomSheetPercentageShown = slideOffset * -1;
+ pickerBackdrop.setBackgroundColor(ArgbEvaluatorCompat.getInstance().evaluate(bottomSheetPercentageShown, backdropColorExpanded, backdropColorCollapsed));
+ bottomNavigation.setTranslationY(bottomSheetPercentageShown * bottomNavigationHeight);
+ if (slideOffset <= lastOffset && slideOffset != 0) {
+ if (fab.getVisibility() == GONE) {
+ fab.show();
+ }
+ } else {
+ if (fab.getVisibility() == VISIBLE) {
+ fab.hide();
+ }
+ }
+ }
+ lastOffset = slideOffset;
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentsFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentsFragment.java
index 1c8a84103..bb42f27ef 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentsFragment.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentsFragment.java
@@ -1,23 +1,32 @@
package it.niedermann.nextcloud.deck.ui.card.attachments;
import android.content.ContentResolver;
+import android.content.Context;
import android.content.Intent;
+import android.content.res.ColorStateList;
import android.net.Uri;
import android.os.Bundle;
+import android.provider.ContactsContract;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.widget.LinearLayout;
import android.widget.Toast;
+import androidx.activity.OnBackPressedCallback;
+import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.SharedElementCallback;
+import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
+import com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.google.android.material.snackbar.Snackbar;
import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException;
@@ -27,6 +36,7 @@ import java.time.Instant;
import java.util.List;
import java.util.Map;
+import it.niedermann.android.util.DimensionUtil;
import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.FragmentCardEditTabAttachmentsBinding;
@@ -38,16 +48,36 @@ import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.WrappedLiv
import it.niedermann.nextcloud.deck.ui.branding.BrandedFragment;
import it.niedermann.nextcloud.deck.ui.branding.BrandedSnackbar;
import it.niedermann.nextcloud.deck.ui.card.EditCardViewModel;
+import it.niedermann.nextcloud.deck.ui.card.attachments.picker.AbstractPickerAdapter;
+import it.niedermann.nextcloud.deck.ui.card.attachments.picker.ContactAdapter;
+import it.niedermann.nextcloud.deck.ui.card.attachments.picker.FileAdapter;
+import it.niedermann.nextcloud.deck.ui.card.attachments.picker.FileAdapterLegacy;
+import it.niedermann.nextcloud.deck.ui.card.attachments.picker.GalleryAdapter;
+import it.niedermann.nextcloud.deck.ui.card.attachments.picker.GalleryItemDecoration;
+import it.niedermann.nextcloud.deck.ui.card.attachments.previewdialog.PreviewDialog;
+import it.niedermann.nextcloud.deck.ui.card.attachments.previewdialog.PreviewDialogViewModel;
import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment;
+import it.niedermann.nextcloud.deck.ui.takephoto.TakePhotoActivity;
+import it.niedermann.nextcloud.deck.util.DeckColorUtil;
+import it.niedermann.nextcloud.deck.util.VCardUtil;
+import static android.Manifest.permission.CAMERA;
+import static android.Manifest.permission.READ_CONTACTS;
import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
import static android.app.Activity.RESULT_OK;
import static android.os.Build.VERSION.SDK_INT;
+import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static android.os.Build.VERSION_CODES.M;
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
import static androidx.core.content.PermissionChecker.PERMISSION_GRANTED;
import static androidx.core.content.PermissionChecker.checkSelfPermission;
+import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED;
+import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HIDDEN;
import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce;
import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.applyBrandToFAB;
+import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.isBrandingEnabled;
+import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.readBrandMainColor;
import static it.niedermann.nextcloud.deck.ui.card.attachments.CardAttachmentAdapter.VIEW_TYPE_DEFAULT;
import static it.niedermann.nextcloud.deck.ui.card.attachments.CardAttachmentAdapter.VIEW_TYPE_IMAGE;
import static it.niedermann.nextcloud.deck.util.AttachmentUtil.copyContentUriToTempFile;
@@ -56,14 +86,36 @@ import static java.net.HttpURLConnection.HTTP_CONFLICT;
public class CardAttachmentsFragment extends BrandedFragment implements AttachmentDeletedListener, AttachmentClickedListener {
private FragmentCardEditTabAttachmentsBinding binding;
- private EditCardViewModel viewModel;
+ private EditCardViewModel editViewModel;
+ private PreviewDialogViewModel previewViewModel;
+ private BottomSheetBehavior<LinearLayout> mBottomSheetBehaviour;
- private static final int REQUEST_CODE_ADD_FILE = 1;
- private static final int REQUEST_CODE_ADD_FILE_PERMISSION = 2;
+ private RecyclerView.ItemDecoration galleryItemDecoration;
+
+ private static final int REQUEST_CODE_PICK_FILE = 1;
+ private static final int REQUEST_CODE_PICK_FILE_PERMISSION = 2;
+ private static final int REQUEST_CODE_PICK_CAMERA = 3;
+ private static final int REQUEST_CODE_PICK_GALLERY_PERMISSION = 4;
+ private static final int REQUEST_CODE_PICK_CONTACT = 5;
+ private static final int REQUEST_CODE_PICK_CONTACT_PICKER_PERMISSION = 6;
+
+ @ColorInt
+ private int accentColor;
+ @ColorInt
+ private int primaryColor;
private SyncManager syncManager;
private CardAttachmentAdapter adapter;
+ private AbstractPickerAdapter<?> pickerAdapter;
+
+ private final OnBackPressedCallback backPressedCallback = new OnBackPressedCallback(true) {
+ @Override
+ public void handleOnBackPressed() {
+ mBottomSheetBehaviour.setState(STATE_HIDDEN);
+ }
+ };
+
private int clickedItemPosition;
@Override
@@ -72,11 +124,24 @@ public class CardAttachmentsFragment extends BrandedFragment implements Attachme
Bundle savedInstanceState) {
binding = FragmentCardEditTabAttachmentsBinding.inflate(inflater, container, false);
- viewModel = new ViewModelProvider(requireActivity()).get(EditCardViewModel.class);
+ editViewModel = new ViewModelProvider(requireActivity()).get(EditCardViewModel.class);
+ previewViewModel = new ViewModelProvider(requireActivity()).get(PreviewDialogViewModel.class);
+ binding.bottomNavigation.setOnNavigationItemSelectedListener(item -> {
+ if (item.getItemId() == R.id.gallery) {
+ showGalleryPicker();
+ } else if (item.getItemId() == R.id.contacts) {
+ showContactPicker();
+ } else if (item.getItemId() == R.id.files) {
+ showFilePicker();
+ }
+ return true;
+ });
+ accentColor = ContextCompat.getColor(requireContext(), R.color.accent);
+ primaryColor = ContextCompat.getColor(requireContext(), R.color.primary);
// This might be a zombie fragment with an empty EditCardViewModel after Android killed the activity (but not the fragment instance
// See https://github.com/stefan-niedermann/nextcloud-deck/issues/478
- if (viewModel.getFullCard() == null) {
+ if (editViewModel.getFullCard() == null) {
DeckLog.logError(new IllegalStateException("Cannot populate " + CardAttachmentsFragment.class.getSimpleName() + " because viewModel.getFullCard() is null"));
return binding.getRoot();
}
@@ -86,15 +151,32 @@ public class CardAttachmentsFragment extends BrandedFragment implements Attachme
getChildFragmentManager(),
requireActivity().getMenuInflater(),
this,
- viewModel.getAccount(),
- viewModel.getFullCard().getLocalId());
+ editViewModel.getAccount(),
+ editViewModel.getFullCard().getLocalId());
binding.attachmentsList.setAdapter(adapter);
- updateEmptyContentView();
+ adapter.isEmpty().observe(getViewLifecycleOwner(), (isEmpty) -> {
+ if (isEmpty) {
+ this.binding.emptyContentView.setVisibility(VISIBLE);
+ this.binding.attachmentsList.setVisibility(GONE);
+ } else {
+ this.binding.emptyContentView.setVisibility(GONE);
+ this.binding.attachmentsList.setVisibility(VISIBLE);
+ }
+ });
+ galleryItemDecoration = new GalleryItemDecoration(DimensionUtil.INSTANCE.dpToPx(requireContext(), R.dimen.spacer_1qx));
+ mBottomSheetBehaviour = BottomSheetBehavior.from(binding.bottomSheetParent);
+ mBottomSheetBehaviour.setDraggable(true);
+ mBottomSheetBehaviour.setHideable(true);
+ mBottomSheetBehaviour.setState(STATE_HIDDEN);
+ mBottomSheetBehaviour.addBottomSheetCallback(new CardAttachmentsBottomsheetBehaviorCallback(
+ requireContext(), backPressedCallback, binding.fab, binding.pickerBackdrop, binding.bottomNavigation,
+ R.color.mdtp_transparent_black, android.R.color.transparent, R.dimen.attachments_bottom_navigation_height));
+ binding.pickerBackdrop.setOnClickListener(v -> mBottomSheetBehaviour.setState(STATE_HIDDEN));
final DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
- int spanCount = (int) ((displayMetrics.widthPixels / displayMetrics.density) / getResources().getInteger(R.integer.max_dp_attachment_column));
- GridLayoutManager glm = new GridLayoutManager(getContext(), spanCount);
+ final int spanCount = (int) ((displayMetrics.widthPixels / displayMetrics.density) / getResources().getInteger(R.integer.max_dp_attachment_column));
+ final GridLayoutManager glm = new GridLayoutManager(getContext(), spanCount);
glm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
@@ -108,7 +190,7 @@ public class CardAttachmentsFragment extends BrandedFragment implements Attachme
}
});
binding.attachmentsList.setLayoutManager(glm);
- if (!viewModel.isCreateMode()) {
+ if (!editViewModel.isCreateMode()) {
// https://android-developers.googleblog.com/2018/02/continuous-shared-element-transitions.html?m=1
// https://github.com/android/animation-samples/blob/master/GridToPager/app/src/main/java/com/google/samples/gridtopager/fragment/ImagePagerFragment.java
setExitSharedElementCallback(new SharedElementCallback() {
@@ -121,12 +203,21 @@ public class CardAttachmentsFragment extends BrandedFragment implements Attachme
}
}
});
- adapter.setAttachments(viewModel.getFullCard().getAttachments(), viewModel.getFullCard().getId());
- updateEmptyContentView();
+ adapter.setAttachments(editViewModel.getFullCard().getAttachments(), editViewModel.getFullCard().getId());
}
- if (viewModel.canEdit()) {
- binding.fab.setOnClickListener(v -> pickFile());
+ if (editViewModel.canEdit()) {
+ binding.fab.setOnClickListener(v -> {
+ if (SDK_INT < LOLLIPOP) {
+ openNativeFilePicker();
+ } else {
+ binding.bottomNavigation.setSelectedItemId(R.id.gallery);
+ showGalleryPicker();
+ mBottomSheetBehaviour.setState(STATE_COLLAPSED);
+ backPressedCallback.setEnabled(true);
+ requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), backPressedCallback);
+ }
+ });
binding.fab.show();
binding.attachmentsList.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
@@ -141,113 +232,264 @@ public class CardAttachmentsFragment extends BrandedFragment implements Attachme
binding.fab.hide();
binding.emptyContentView.hideDescription();
}
+ @Nullable Context context = requireContext();
+ if (isBrandingEnabled(context)) {
+ applyBrand(readBrandMainColor(context));
+ }
return binding.getRoot();
}
- public void pickFile() {
- if (SDK_INT >= M && checkSelfPermission(requireActivity(), READ_EXTERNAL_STORAGE) != PERMISSION_GRANTED) {
- requestPermissions(new String[]{READ_EXTERNAL_STORAGE}, REQUEST_CODE_ADD_FILE_PERMISSION);
+ @Override
+ public void onPause() {
+ super.onPause();
+ backPressedCallback.setEnabled(false);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ backPressedCallback.setEnabled(binding.bottomNavigation.getTranslationY() == 0);
+ }
+
+ private void showGalleryPicker() {
+ if (!(pickerAdapter instanceof GalleryAdapter)) {
+ if (isPermissionRequestNeeded(READ_EXTERNAL_STORAGE) || isPermissionRequestNeeded(CAMERA)) {
+ requestPermissions(new String[]{READ_EXTERNAL_STORAGE, CAMERA}, REQUEST_CODE_PICK_GALLERY_PERMISSION);
+ } else {
+ unbindPickerAdapter();
+ pickerAdapter = new GalleryAdapter(requireContext(), (uri, pair) -> {
+ previewViewModel.prepareDialog(pair.first, pair.second);
+ PreviewDialog.newInstance().show(getChildFragmentManager(), PreviewDialog.class.getSimpleName());
+ observeOnce(previewViewModel.getResult(), getViewLifecycleOwner(), (submitPositive) -> {
+ if (submitPositive) {
+ onActivityResult(REQUEST_CODE_PICK_FILE, RESULT_OK, new Intent().setData(uri));
+ }
+ });
+ }, this::openNativeCameraPicker, getViewLifecycleOwner());
+ if (binding.pickerRecyclerView.getItemDecorationCount() == 0) {
+ binding.pickerRecyclerView.addItemDecoration(galleryItemDecoration);
+ }
+ binding.pickerRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), 3));
+ binding.pickerRecyclerView.setAdapter(pickerAdapter);
+ }
+ }
+ }
+
+ private void showContactPicker() {
+ if (!(pickerAdapter instanceof ContactAdapter)) {
+ if (isPermissionRequestNeeded(READ_CONTACTS)) {
+ requestPermissions(new String[]{READ_CONTACTS}, REQUEST_CODE_PICK_CONTACT_PICKER_PERMISSION);
+ } else {
+ unbindPickerAdapter();
+ pickerAdapter = new ContactAdapter(requireContext(), (uri, pair) -> {
+ previewViewModel.prepareDialog(pair.first, pair.second);
+ PreviewDialog.newInstance().show(getChildFragmentManager(), PreviewDialog.class.getSimpleName());
+ observeOnce(previewViewModel.getResult(), getViewLifecycleOwner(), (submitPositive) -> {
+ if (submitPositive) {
+ onActivityResult(REQUEST_CODE_PICK_CONTACT, RESULT_OK, new Intent().setData(uri));
+ }
+ });
+ }, this::openNativeContactPicker);
+ removeGalleryItemDecoration();
+ binding.pickerRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
+ binding.pickerRecyclerView.setAdapter(pickerAdapter);
+ }
+ }
+ }
+
+ private void showFilePicker() {
+ if (!(pickerAdapter instanceof FileAdapter) && !(pickerAdapter instanceof FileAdapterLegacy)) {
+ if (isPermissionRequestNeeded(READ_EXTERNAL_STORAGE)) {
+ requestPermissions(new String[]{READ_EXTERNAL_STORAGE}, REQUEST_CODE_PICK_FILE_PERMISSION);
+ } else {
+ unbindPickerAdapter();
+ if (SDK_INT >= LOLLIPOP) {
+// if (SDK_INT >= Build.VERSION_CODES.Q) {
+// // TODO Only usable with Scoped Storage
+// pickerAdapter = new FileAdapter(requireContext(), uri -> onActivityResult(REQUEST_CODE_PICK_FILE, RESULT_OK, new Intent().setData(uri)), this::openNativeFilePicker);
+// } else {
+ pickerAdapter = new FileAdapterLegacy((uri, pair) -> {
+ previewViewModel.prepareDialog(pair.first, pair.second);
+ PreviewDialog.newInstance().show(getChildFragmentManager(), PreviewDialog.class.getSimpleName());
+ observeOnce(previewViewModel.getResult(), getViewLifecycleOwner(), (submitPositive) -> {
+ if (submitPositive) {
+ onActivityResult(REQUEST_CODE_PICK_FILE, RESULT_OK, new Intent().setData(uri));
+ }
+ });
+ }, this::openNativeFilePicker);
+// }
+ removeGalleryItemDecoration();
+ binding.pickerRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
+ binding.pickerRecyclerView.setAdapter(pickerAdapter);
+ }
+ }
+ }
+ }
+
+ private void openNativeCameraPicker() {
+ if (SDK_INT >= LOLLIPOP) {
+ startActivityForResult(TakePhotoActivity.createIntent(requireContext()), REQUEST_CODE_PICK_CAMERA);
} else {
- startActivityForResult(new Intent(Intent.ACTION_GET_CONTENT)
- .addCategory(Intent.CATEGORY_OPENABLE)
- .setType("*/*"), REQUEST_CODE_ADD_FILE);
+ ExceptionDialogFragment.newInstance(new UnsupportedOperationException("This feature requires Android 5"), editViewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ }
+ }
+
+ private void openNativeContactPicker() {
+ final Intent intent = new Intent(Intent.ACTION_PICK).setType(ContactsContract.Contacts.CONTENT_TYPE);
+ if (intent.resolveActivity(requireContext().getPackageManager()) != null) {
+ startActivityForResult(intent, REQUEST_CODE_PICK_CONTACT);
+ }
+ }
+
+ private void openNativeFilePicker() {
+ startActivityForResult(new Intent(Intent.ACTION_GET_CONTENT)
+ .addCategory(Intent.CATEGORY_OPENABLE)
+ .setType("*/*"), REQUEST_CODE_PICK_FILE);
+ }
+
+ /**
+ * Checks the current Android version and whether the permission has already been granted.
+ *
+ * @param permission see {@link android.Manifest.permission}
+ * @return whether or not requesting permission is needed
+ */
+ private boolean isPermissionRequestNeeded(@NonNull String permission) {
+ return SDK_INT >= M && checkSelfPermission(requireActivity(), permission) != PERMISSION_GRANTED;
+ }
+
+ private void unbindPickerAdapter() {
+ if (pickerAdapter != null) {
+ pickerAdapter.onDestroy();
+ }
+ }
+
+ private void removeGalleryItemDecoration() {
+ if (binding.pickerRecyclerView.getItemDecorationCount() > 0) {
+ binding.pickerRecyclerView.removeItemDecoration(galleryItemDecoration);
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
- //noinspection SwitchStatementWithTooFewBranches
+ super.onActivityResult(requestCode, resultCode, data);
switch (requestCode) {
- case REQUEST_CODE_ADD_FILE: {
+ case REQUEST_CODE_PICK_CONTACT:
+ case REQUEST_CODE_PICK_CAMERA:
+ case REQUEST_CODE_PICK_FILE: {
if (resultCode == RESULT_OK) {
- if (data == null) {
- ExceptionDialogFragment.newInstance(new UploadAttachmentFailedException("Intent data is null"), viewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
- return;
- }
- final Uri sourceUri = data.getData();
- if (sourceUri == null) {
- ExceptionDialogFragment.newInstance(new UploadAttachmentFailedException("sourceUri is null"), viewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
- return;
- }
- if (!ContentResolver.SCHEME_CONTENT.equals(sourceUri.getScheme())) {
- ExceptionDialogFragment.newInstance(new UploadAttachmentFailedException("Unknown URI scheme: " + sourceUri.getScheme()), viewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
- return;
+ final Uri sourceUri = requestCode == REQUEST_CODE_PICK_CONTACT
+ ? VCardUtil.getVCardContentUri(requireContext(), Uri.parse(data.getDataString()))
+ : data.getData();
+ try {
+ uploadNewAttachmentFromUri(sourceUri, requestCode == REQUEST_CODE_PICK_CAMERA
+ ? data.getType()
+ : requireContext().getContentResolver().getType(sourceUri));
+ mBottomSheetBehaviour.setState(STATE_HIDDEN);
+ } catch (Exception e) {
+ ExceptionDialogFragment.newInstance(e, editViewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
}
+ }
+ break;
+ }
+ default: {
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+ }
+ }
- DeckLog.verbose("--- found content URL " + sourceUri.getPath());
- File fileToUpload;
+ @Override
+ public void onDestroy() {
+ if (this.pickerAdapter != null) {
+ this.pickerAdapter.onDestroy();
+ this.binding.pickerRecyclerView.setAdapter(null);
+ }
+ super.onDestroy();
+ }
- try {
- DeckLog.verbose("---- so, now copy & upload: " + sourceUri.getPath());
- fileToUpload = copyContentUriToTempFile(requireContext(), sourceUri, viewModel.getAccount().getId(), viewModel.getFullCard().getCard().getLocalId());
- } catch (IllegalArgumentException | IOException e) {
- ExceptionDialogFragment.newInstance(new UploadAttachmentFailedException("Could not copy content URI to temporary file", e), viewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ private void uploadNewAttachmentFromUri(@NonNull Uri sourceUri, String mimeType) throws UploadAttachmentFailedException, IOException {
+ if (sourceUri == null) {
+ throw new UploadAttachmentFailedException("sourceUri is null");
+ }
+ switch (sourceUri.getScheme()) {
+ case ContentResolver.SCHEME_CONTENT:
+ case ContentResolver.SCHEME_FILE: {
+ DeckLog.verbose("--- found content URL " + sourceUri.getPath());
+ final File fileToUpload = copyContentUriToTempFile(requireContext(), sourceUri, editViewModel.getAccount().getId(), editViewModel.getFullCard().getLocalId());
+ for (Attachment existingAttachment : editViewModel.getFullCard().getAttachments()) {
+ final String existingPath = existingAttachment.getLocalPath();
+ if (existingPath != null && existingPath.equals(fileToUpload.getAbsolutePath())) {
+ BrandedSnackbar.make(binding.coordinatorLayout, R.string.attachment_already_exists, Snackbar.LENGTH_LONG).show();
return;
}
-
- for (Attachment existingAttachment : viewModel.getFullCard().getAttachments()) {
- final String existingPath = existingAttachment.getLocalPath();
- if (existingPath != null && existingPath.equals(fileToUpload.getAbsolutePath())) {
- BrandedSnackbar.make(binding.coordinatorLayout, R.string.attachment_already_exists, Snackbar.LENGTH_LONG).show();
- return;
- }
- }
-
- final Instant now = Instant.now();
- final Attachment a = new Attachment();
- a.setMimetype(requireContext().getContentResolver().getType(sourceUri));
- a.setData(fileToUpload.getName());
- a.setFilename(fileToUpload.getName());
- a.setBasename(fileToUpload.getName());
- a.setFilesize(fileToUpload.length());
- a.setLocalPath(fileToUpload.getAbsolutePath());
- a.setLastModifiedLocal(now);
- a.setStatusEnum(DBStatus.LOCAL_EDITED);
- a.setCreatedAt(now);
- viewModel.getFullCard().getAttachments().add(a);
- adapter.addAttachment(a);
- if (!viewModel.isCreateMode()) {
- WrappedLiveData<Attachment> liveData = syncManager.addAttachmentToCard(viewModel.getAccount().getId(), viewModel.getFullCard().getLocalId(), a.getMimetype(), fileToUpload);
- observeOnce(liveData, getViewLifecycleOwner(), (next) -> {
- if (liveData.hasError()) {
- Throwable t = liveData.getError();
- if (t instanceof NextcloudHttpRequestFailedException && ((NextcloudHttpRequestFailedException) t).getStatusCode() == HTTP_CONFLICT) {
- // https://github.com/stefan-niedermann/nextcloud-deck/issues/534
- viewModel.getFullCard().getAttachments().remove(a);
- adapter.removeAttachment(a);
- BrandedSnackbar.make(binding.coordinatorLayout, R.string.attachment_already_exists, Snackbar.LENGTH_LONG).show();
- } else {
- ExceptionDialogFragment.newInstance(new UploadAttachmentFailedException("Unknown URI scheme", t), viewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
- }
- } else {
- viewModel.getFullCard().getAttachments().remove(a);
+ }
+ final Instant now = Instant.now();
+ final Attachment a = new Attachment();
+ a.setMimetype(mimeType);
+ a.setData(fileToUpload.getName());
+ a.setFilename(fileToUpload.getName());
+ a.setBasename(fileToUpload.getName());
+ a.setFilesize(fileToUpload.length());
+ a.setLocalPath(fileToUpload.getAbsolutePath());
+ a.setLastModifiedLocal(now);
+ a.setCreatedAt(now);
+ a.setStatusEnum(DBStatus.LOCAL_EDITED);
+ editViewModel.getFullCard().getAttachments().add(0, a);
+ adapter.addAttachment(a);
+ if (!editViewModel.isCreateMode()) {
+ WrappedLiveData<Attachment> liveData = syncManager.addAttachmentToCard(editViewModel.getAccount().getId(), editViewModel.getFullCard().getLocalId(), a.getMimetype(), fileToUpload);
+ observeOnce(liveData, getViewLifecycleOwner(), (next) -> {
+ if (liveData.hasError()) {
+ Throwable t = liveData.getError();
+ if (t instanceof NextcloudHttpRequestFailedException && ((NextcloudHttpRequestFailedException) t).getStatusCode() == HTTP_CONFLICT) {
+ // https://github.com/stefan-niedermann/nextcloud-deck/issues/534
+ editViewModel.getFullCard().getAttachments().remove(a);
adapter.removeAttachment(a);
- viewModel.getFullCard().getAttachments().add(next);
- adapter.addAttachment(next);
+ BrandedSnackbar.make(binding.coordinatorLayout, R.string.attachment_already_exists, Snackbar.LENGTH_LONG).show();
+ } else {
+ ExceptionDialogFragment.newInstance(new UploadAttachmentFailedException("Unknown URI scheme", t), editViewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
}
- });
- }
- updateEmptyContentView();
+ } else {
+ editViewModel.getFullCard().getAttachments().remove(a);
+ editViewModel.getFullCard().getAttachments().add(0, next);
+ adapter.replaceAttachment(a, next);
+ }
+ });
}
break;
}
default: {
- super.onActivityResult(requestCode, resultCode, data);
+ throw new UploadAttachmentFailedException("Unknown URI scheme: " + sourceUri.getScheme());
}
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
- //noinspection SwitchStatementWithTooFewBranches
switch (requestCode) {
- case REQUEST_CODE_ADD_FILE_PERMISSION:
+ case REQUEST_CODE_PICK_FILE_PERMISSION: {
if (checkSelfPermission(requireActivity(), READ_EXTERNAL_STORAGE) == PERMISSION_GRANTED) {
- pickFile();
+ showFilePicker();
+ } else {
+ Toast.makeText(requireContext(), R.string.cannot_upload_files_without_permission, Toast.LENGTH_LONG).show();
+ }
+ break;
+ }
+ case REQUEST_CODE_PICK_GALLERY_PERMISSION: {
+ if (checkSelfPermission(requireActivity(), READ_EXTERNAL_STORAGE) == PERMISSION_GRANTED && checkSelfPermission(requireActivity(), CAMERA) == PERMISSION_GRANTED) {
+ showGalleryPicker();
+ } else {
+ Toast.makeText(requireContext(), R.string.cannot_upload_files_without_permission, Toast.LENGTH_LONG).show();
+ }
+ break;
+ }
+ case REQUEST_CODE_PICK_CONTACT_PICKER_PERMISSION: {
+ if (checkSelfPermission(requireActivity(), READ_CONTACTS) == PERMISSION_GRANTED) {
+ showContactPicker();
} else {
Toast.makeText(requireContext(), R.string.cannot_upload_files_without_permission, Toast.LENGTH_LONG).show();
}
break;
+ }
default:
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
@@ -256,16 +498,15 @@ public class CardAttachmentsFragment extends BrandedFragment implements Attachme
@Override
public void onAttachmentDeleted(Attachment attachment) {
adapter.removeAttachment(attachment);
- viewModel.getFullCard().getAttachments().remove(attachment);
- if (!viewModel.isCreateMode() && attachment.getLocalId() != null) {
- final WrappedLiveData<Void> deleteLiveData = syncManager.deleteAttachmentOfCard(viewModel.getAccount().getId(), viewModel.getFullCard().getLocalId(), attachment.getLocalId());
+ editViewModel.getFullCard().getAttachments().remove(attachment);
+ if (!editViewModel.isCreateMode() && attachment.getLocalId() != null) {
+ final WrappedLiveData<Void> deleteLiveData = syncManager.deleteAttachmentOfCard(editViewModel.getAccount().getId(), editViewModel.getFullCard().getLocalId(), attachment.getLocalId());
observeOnce(deleteLiveData, this, (next) -> {
if (deleteLiveData.hasError() && !SyncManager.ignoreExceptionOnVoidError(deleteLiveData.getError())) {
- ExceptionDialogFragment.newInstance(deleteLiveData.getError(), viewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ ExceptionDialogFragment.newInstance(deleteLiveData.getError(), editViewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
}
});
}
- updateEmptyContentView();
}
@Override
@@ -273,20 +514,25 @@ public class CardAttachmentsFragment extends BrandedFragment implements Attachme
this.clickedItemPosition = position;
}
- private void updateEmptyContentView() {
- if (this.adapter == null || this.adapter.getItemCount() == 0) {
- this.binding.emptyContentView.setVisibility(View.VISIBLE);
- this.binding.attachmentsList.setVisibility(View.GONE);
- } else {
- this.binding.emptyContentView.setVisibility(View.GONE);
- this.binding.attachmentsList.setVisibility(View.VISIBLE);
- }
- }
-
@Override
public void applyBrand(int mainColor) {
applyBrandToFAB(mainColor, binding.fab);
adapter.applyBrand(mainColor);
+ @ColorInt final int finalMainColor = DeckColorUtil.contrastRatioIsSufficient(mainColor, primaryColor)
+ ? mainColor
+ : accentColor;
+ final ColorStateList list = new ColorStateList(
+ new int[][]{
+ new int[]{android.R.attr.state_checked},
+ new int[]{}
+ },
+ new int[]{
+ finalMainColor,
+ accentColor
+ }
+ );
+ binding.bottomNavigation.setItemIconTintList(list);
+ binding.bottomNavigation.setItemTextColor(list);
}
public static Fragment newInstance() {
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/DefaultAttachmentViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/DefaultAttachmentViewHolder.java
index 3f7287c24..2b5358eb9 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/DefaultAttachmentViewHolder.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/DefaultAttachmentViewHolder.java
@@ -10,16 +10,16 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentManager;
-import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.ItemAttachmentDefaultBinding;
import it.niedermann.nextcloud.deck.model.Account;
import it.niedermann.nextcloud.deck.model.Attachment;
-import it.niedermann.nextcloud.deck.util.AttachmentUtil;
import it.niedermann.nextcloud.deck.util.DateUtil;
-import it.niedermann.nextcloud.deck.util.MimeTypeUtil;
+
+import static it.niedermann.nextcloud.deck.util.AttachmentUtil.getIconForMimeType;
+import static it.niedermann.nextcloud.deck.util.AttachmentUtil.openAttachmentInBrowser;
public class DefaultAttachmentViewHolder extends AttachmentViewHolder {
- private ItemAttachmentDefaultBinding binding;
+ private final ItemAttachmentDefaultBinding binding;
@SuppressWarnings("WeakerAccess")
public DefaultAttachmentViewHolder(ItemAttachmentDefaultBinding binding) {
@@ -39,20 +39,8 @@ public class DefaultAttachmentViewHolder extends AttachmentViewHolder {
public void bind(@NonNull Account account, @NonNull MenuInflater menuInflater, @NonNull FragmentManager fragmentManager, Long cardRemoteId, Attachment attachment, @Nullable View.OnClickListener onClickListener, @ColorInt int mainColor) {
super.bind(account, menuInflater, fragmentManager, cardRemoteId, attachment, onClickListener, mainColor);
-
- if (MimeTypeUtil.isAudio(attachment.getMimetype())) {
- getPreview().setImageResource(R.drawable.ic_music_note_grey600_24dp);
- } else if (MimeTypeUtil.isVideo(attachment.getMimetype())) {
- getPreview().setImageResource(R.drawable.ic_local_movies_grey600_24dp);
- } else if (MimeTypeUtil.isPdf(attachment.getMimetype())) {
- getPreview().setImageResource(R.drawable.ic_baseline_picture_as_pdf_24);
- } else if (MimeTypeUtil.isContact(attachment.getMimetype())) {
- getPreview().setImageResource(R.drawable.ic_baseline_contact_mail_24);
- } else {
- getPreview().setImageResource(R.drawable.ic_attach_file_grey600_24dp);
- }
-
- itemView.setOnClickListener((event) -> AttachmentUtil.openAttachmentInBrowser(itemView.getContext(), account.getUrl(), cardRemoteId, attachment.getId()));
+ getPreview().setImageResource(getIconForMimeType(attachment.getMimetype()));
+ itemView.setOnClickListener((event) -> openAttachmentInBrowser(itemView.getContext(), account.getUrl(), cardRemoteId, attachment.getId()));
binding.filename.setText(attachment.getBasename());
binding.filesize.setText(Formatter.formatFileSize(binding.filesize.getContext(), attachment.getFilesize()));
if (attachment.getLastModifiedLocal() != null) {
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/AbstractCursorPickerAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/AbstractCursorPickerAdapter.java
new file mode 100644
index 000000000..a2ea6dd37
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/AbstractCursorPickerAdapter.java
@@ -0,0 +1,100 @@
+package it.niedermann.nextcloud.deck.ui.card.attachments.picker;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.util.Pair;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.bumptech.glide.RequestBuilder;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.function.BiConsumer;
+
+import static android.database.Cursor.FIELD_TYPE_INTEGER;
+import static android.database.Cursor.FIELD_TYPE_NULL;
+import static androidx.recyclerview.widget.RecyclerView.NO_ID;
+import static java.util.Objects.requireNonNull;
+
+/**
+ * An {@link RecyclerView.Adapter} which provides previews of one type of files and also an option to open a native dialog.
+ * <p>
+ * Example: Previews for images of the gallery as well a one option to take a photo
+ */
+public abstract class AbstractCursorPickerAdapter<T extends RecyclerView.ViewHolder> extends AbstractPickerAdapter<T> {
+
+ private final int count;
+ protected final int columnIndex;
+ private final int columnIndexType;
+ @NonNull
+ protected final BiConsumer<Uri, Pair<String, RequestBuilder<?>>> onSelect;
+ @NonNull
+ protected final Runnable openNativePicker;
+ @NonNull
+ protected final Cursor cursor;
+ @NonNull
+ protected final ContentResolver contentResolver;
+
+ /**
+ * Should be used to bind heavy operations like when dealing with {@link Bitmap}.
+ * This must only be one {@link Thread} because otherwise the cursor might change while fetching data from it.
+ */
+ @NonNull
+ protected final ExecutorService bindExecutor = Executors.newFixedThreadPool(1);
+
+ public AbstractCursorPickerAdapter(@NonNull Context context, @NonNull BiConsumer<Uri, Pair<String, RequestBuilder<?>>> onSelect, @NonNull Runnable openNativePicker, Uri subject, String idColumn, String sortOrder) {
+ this(context, onSelect, openNativePicker, subject, idColumn, new String[]{idColumn}, sortOrder);
+ }
+
+ public AbstractCursorPickerAdapter(@NonNull Context context, @NonNull BiConsumer<Uri, Pair<String, RequestBuilder<?>>> onSelect, @NonNull Runnable openNativePicker, Uri subject, String idColumn, String[] requestedColumns, String sortOrder) {
+ this(context, onSelect, openNativePicker, idColumn, requireNonNull(context.getContentResolver().query(subject, requestedColumns, null, null, sortOrder)));
+ }
+
+ public AbstractCursorPickerAdapter(@NonNull Context context, @NonNull BiConsumer<Uri, Pair<String, RequestBuilder<?>>> onSelect, @NonNull Runnable openNativePicker, String idColumn, @NonNull Cursor cursor) {
+ this.contentResolver = context.getContentResolver();
+ this.onSelect = onSelect;
+ this.openNativePicker = openNativePicker;
+ this.cursor = cursor;
+ this.cursor.moveToFirst();
+ this.columnIndex = this.cursor.getColumnIndex(idColumn);
+ this.count = cursor.getCount() + 1;
+ this.columnIndexType = (this.count > 1) ? this.cursor.getType(columnIndex) : FIELD_TYPE_NULL;
+ setHasStableIds(true);
+ }
+
+ /**
+ * Moves the {@link #cursor} to the given position
+ */
+ @Override
+ public long getItemId(int position) {
+ if (!cursor.isClosed() && cursor.moveToPosition(position - 1)) {
+ //noinspection SwitchStatementWithTooFewBranches
+ switch (columnIndexType) {
+ case FIELD_TYPE_INTEGER:
+ return cursor.getLong(columnIndex);
+ default:
+ throw new IllegalStateException("Unknown type for columnIndex \"" + columnIndex + "\": " + columnIndexType);
+ }
+ } else {
+ return NO_ID;
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ return count;
+ }
+
+ /**
+ * Call this method when the {@link AbstractCursorPickerAdapter} is no longer need to free resources.
+ */
+ public void onDestroy() {
+ cursor.close();
+ bindExecutor.shutdownNow();
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/AbstractPickerAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/AbstractPickerAdapter.java
new file mode 100644
index 000000000..901d204cd
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/AbstractPickerAdapter.java
@@ -0,0 +1,26 @@
+package it.niedermann.nextcloud.deck.ui.card.attachments.picker;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+public abstract class AbstractPickerAdapter<T extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<T> {
+
+ protected static final int VIEW_TYPE_NONE = -1;
+ protected static final int VIEW_TYPE_ITEM = 0;
+ protected static final int VIEW_TYPE_ITEM_NATIVE = 1;
+
+ @Override
+ public int getItemViewType(int position) {
+ if (position > 0) {
+ return VIEW_TYPE_ITEM;
+ } else if (position == 0) {
+ return VIEW_TYPE_ITEM_NATIVE;
+ } else {
+ return VIEW_TYPE_NONE;
+ }
+ }
+
+ /**
+ * Call this method when the {@link AbstractPickerAdapter} is no longer need to free resources.
+ */
+ public abstract void onDestroy();
+} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/ContactAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/ContactAdapter.java
new file mode 100644
index 000000000..22ac0c694
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/ContactAdapter.java
@@ -0,0 +1,104 @@
+package it.niedermann.nextcloud.deck.ui.card.attachments.picker;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.provider.ContactsContract;
+import android.text.TextUtils;
+import android.util.Pair;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.bumptech.glide.RequestBuilder;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.function.BiConsumer;
+
+import it.niedermann.nextcloud.deck.databinding.ItemPickerNativeBinding;
+import it.niedermann.nextcloud.deck.databinding.ItemPickerUserBinding;
+
+import static android.provider.ContactsContract.CommonDataKinds.Email.DATA;
+import static android.provider.ContactsContract.CommonDataKinds.Phone.LOOKUP_KEY;
+import static android.provider.ContactsContract.CommonDataKinds.Phone.NUMBER;
+import static android.provider.ContactsContract.Contacts.CONTENT_LOOKUP_URI;
+import static android.provider.ContactsContract.Contacts.CONTENT_URI;
+import static android.provider.ContactsContract.Contacts.DISPLAY_NAME;
+import static android.provider.ContactsContract.Contacts.SORT_KEY_PRIMARY;
+import static android.provider.ContactsContract.Contacts._ID;
+
+public class ContactAdapter extends AbstractCursorPickerAdapter<RecyclerView.ViewHolder> {
+
+ private final int lookupKeyColumnIndex;
+ private final int displayNameColumnIndex;
+
+ public ContactAdapter(@NonNull Context context, @NonNull BiConsumer<Uri, Pair<String, RequestBuilder<?>>> onSelect, @NonNull Runnable onSelectPicker) {
+ super(context, onSelect, onSelectPicker, CONTENT_URI, _ID, new String[]{_ID, LOOKUP_KEY, DISPLAY_NAME}, SORT_KEY_PRIMARY);
+ lookupKeyColumnIndex = cursor.getColumnIndex(LOOKUP_KEY);
+ displayNameColumnIndex = cursor.getColumnIndex(DISPLAY_NAME);
+ notifyItemRangeInserted(0, getItemCount() + 1);
+ }
+
+ @NonNull
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ switch (viewType) {
+ case VIEW_TYPE_ITEM_NATIVE:
+ return new ContactNativeItemViewHolder(ItemPickerNativeBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false));
+ case VIEW_TYPE_ITEM:
+ return new ContactItemViewHolder(ItemPickerUserBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false));
+ default:
+ throw new IllegalStateException("Unknown viewType " + viewType);
+ }
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
+ switch (getItemViewType(position)) {
+ case VIEW_TYPE_ITEM_NATIVE: {
+ ((ContactNativeItemViewHolder) holder).bind(openNativePicker);
+ break;
+ }
+ case VIEW_TYPE_ITEM: {
+ final ContactItemViewHolder viewHolder = (ContactItemViewHolder) holder;
+ if (!cursor.isClosed()) {
+ cursor.moveToPosition(position - 1);
+ final String displayName = cursor.getString(displayNameColumnIndex);
+ final String lookupKey = cursor.getString(lookupKeyColumnIndex);
+ bindExecutor.execute(() -> {
+ try (InputStream inputStream = ContactsContract.Contacts.openContactPhotoInputStream(contentResolver, Uri.withAppendedPath(CONTENT_LOOKUP_URI, lookupKey))) {
+ final Bitmap thumbnail = BitmapFactory.decodeStream(inputStream);
+ String contactInformation = "";
+ try (final Cursor phoneCursor = contentResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, new String[]{NUMBER}, LOOKUP_KEY + " = ?", new String[]{lookupKey}, null)) {
+ if (phoneCursor != null && phoneCursor.moveToFirst()) {
+ contactInformation = phoneCursor.getString(phoneCursor.getColumnIndex(NUMBER));
+ }
+ }
+ if (TextUtils.isEmpty(contactInformation)) {
+ try (final Cursor emailCursor = contentResolver.query(ContactsContract.CommonDataKinds.Email.CONTENT_URI, new String[]{DATA}, LOOKUP_KEY + " = ?", new String[]{lookupKey}, null)) {
+ if (emailCursor != null && emailCursor.moveToFirst()) {
+ contactInformation = emailCursor.getString(emailCursor.getColumnIndex(DATA));
+ }
+ }
+ }
+ final String finalContactInformation = contactInformation;
+ new Handler(Looper.getMainLooper()).post(() -> viewHolder.bind(Uri.withAppendedPath(CONTENT_LOOKUP_URI, lookupKey), thumbnail, displayName, finalContactInformation, onSelect));
+ } catch (IOException ignored) {
+ new Handler(Looper.getMainLooper()).post(viewHolder::bindError);
+ }
+ });
+ } else {
+ new Handler(Looper.getMainLooper()).post(viewHolder::bindError);
+ }
+ break;
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/ContactItemViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/ContactItemViewHolder.java
new file mode 100644
index 000000000..f403fed21
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/ContactItemViewHolder.java
@@ -0,0 +1,66 @@
+package it.niedermann.nextcloud.deck.ui.card.attachments.picker;
+
+import android.graphics.Bitmap;
+import android.graphics.drawable.ColorDrawable;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.util.Pair;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.RequestBuilder;
+import com.bumptech.glide.request.RequestOptions;
+
+import java.util.function.BiConsumer;
+
+import it.niedermann.nextcloud.deck.R;
+import it.niedermann.nextcloud.deck.databinding.ItemPickerUserBinding;
+
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+import static it.niedermann.nextcloud.deck.util.VCardUtil.getColorBasedOnDisplayName;
+
+public class ContactItemViewHolder extends RecyclerView.ViewHolder {
+
+ private final ItemPickerUserBinding binding;
+
+ public ContactItemViewHolder(@NonNull ItemPickerUserBinding binding) {
+ super(binding.getRoot());
+ this.binding = binding;
+ }
+
+ public void bind(@NonNull Uri uri, @Nullable Bitmap image, @NonNull String displayName, @Nullable String contactInformation, @NonNull BiConsumer<Uri, Pair<String, RequestBuilder<?>>> onSelect) {
+ itemView.setOnClickListener((v) -> onSelect.accept(uri, new Pair<>(displayName, image == null ? null : Glide.with(itemView.getContext()).load(image))));
+ binding.title.setText(displayName);
+ binding.contactInformation.setText(contactInformation);
+ if (image == null) {
+ binding.initials.setVisibility(VISIBLE);
+ binding.initials.setText(TextUtils.isEmpty(displayName)
+ ? null
+ : String.valueOf(displayName.charAt(0))
+ );
+ Glide.with(itemView.getContext())
+ .load(new ColorDrawable(getColorBasedOnDisplayName(itemView.getContext(), displayName)))
+ .apply(RequestOptions.circleCropTransform())
+ .into(binding.avatar);
+ } else {
+ binding.initials.setVisibility(GONE);
+ binding.initials.setText(null);
+ Glide.with(itemView.getContext())
+ .load(image)
+ .placeholder(R.drawable.ic_person_grey600_24dp)
+ .apply(RequestOptions.circleCropTransform())
+ .into(binding.avatar);
+ }
+ }
+
+ public void bindError() {
+ itemView.setOnClickListener(null);
+ Glide.with(itemView.getContext())
+ .load(R.drawable.ic_person_grey600_24dp)
+ .into(binding.avatar);
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/ContactNativeItemViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/ContactNativeItemViewHolder.java
new file mode 100644
index 000000000..a1d7d5921
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/ContactNativeItemViewHolder.java
@@ -0,0 +1,23 @@
+package it.niedermann.nextcloud.deck.ui.card.attachments.picker;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import it.niedermann.nextcloud.deck.R;
+import it.niedermann.nextcloud.deck.databinding.ItemPickerNativeBinding;
+
+public class ContactNativeItemViewHolder extends RecyclerView.ViewHolder {
+
+ private final ItemPickerNativeBinding binding;
+
+ public ContactNativeItemViewHolder(@NonNull ItemPickerNativeBinding binding) {
+ super(binding.getRoot());
+ this.binding = binding;
+ }
+
+ public void bind(@NonNull Runnable onOpenMajorPicker) {
+ binding.title.setText(R.string.show_all_contacts);
+ binding.subtitle.setText(R.string.contacts);
+ itemView.setOnClickListener((v) -> onOpenMajorPicker.run());
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileAdapter.java
new file mode 100644
index 000000000..aa96a0e69
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileAdapter.java
@@ -0,0 +1,85 @@
+package it.niedermann.nextcloud.deck.ui.card.attachments.picker;
+
+import android.content.ContentUris;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.provider.MediaStore;
+import android.util.Pair;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.bumptech.glide.RequestBuilder;
+
+import java.util.function.BiConsumer;
+
+import it.niedermann.nextcloud.deck.databinding.ItemAttachmentDefaultBinding;
+import it.niedermann.nextcloud.deck.databinding.ItemPickerNativeBinding;
+
+import static android.provider.MediaStore.Downloads.DATE_ADDED;
+import static android.provider.MediaStore.Downloads.DATE_MODIFIED;
+import static android.provider.MediaStore.Downloads.EXTERNAL_CONTENT_URI;
+import static android.provider.MediaStore.Downloads.MIME_TYPE;
+import static android.provider.MediaStore.Downloads.SIZE;
+import static android.provider.MediaStore.Downloads.TITLE;
+import static android.provider.MediaStore.Downloads._ID;
+import static java.util.Objects.requireNonNull;
+
+@RequiresApi(api = 29)
+public class FileAdapter extends AbstractCursorPickerAdapter<RecyclerView.ViewHolder> {
+
+ private final int displayNameColumnIndex;
+ private final int sizeColumnIndex;
+ private final int modifiedColumnIndex;
+ private final int mimeTypeColumnIndex;
+
+ private FileAdapter(@NonNull Context context, @NonNull BiConsumer<Uri, Pair<String, RequestBuilder<?>>> onSelect, @NonNull Runnable onSelectPicker) {
+ super(context, onSelect, onSelectPicker, _ID, requireNonNull(context.getContentResolver().query(EXTERNAL_CONTENT_URI, new String[]{_ID, TITLE, SIZE, DATE_MODIFIED, MIME_TYPE}, null, null, DATE_ADDED + " DESC")));
+ displayNameColumnIndex = cursor.getColumnIndex(TITLE);
+ sizeColumnIndex = cursor.getColumnIndex(SIZE);
+ modifiedColumnIndex = cursor.getColumnIndex(DATE_MODIFIED);
+ mimeTypeColumnIndex = cursor.getColumnIndex(MIME_TYPE);
+ notifyItemRangeInserted(0, getItemCount() + 1);
+ }
+
+ @NonNull
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ switch (viewType) {
+ case VIEW_TYPE_ITEM_NATIVE:
+ return new FileNativeItemViewHolder(ItemPickerNativeBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false));
+ case VIEW_TYPE_ITEM:
+ return new FileItemViewHolder(ItemAttachmentDefaultBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false));
+ default:
+ throw new IllegalStateException("Unknown viewType " + viewType);
+ }
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
+ switch (getItemViewType(position)) {
+ case VIEW_TYPE_ITEM_NATIVE: {
+ ((FileNativeItemViewHolder) holder).bind(openNativePicker);
+ break;
+ }
+ case VIEW_TYPE_ITEM: {
+ if (!cursor.isClosed()) {
+ bindExecutor.execute(() -> {
+ final long id = getItemId(position);
+ final String name = cursor.getString(displayNameColumnIndex);
+ final String mimeType = cursor.getString(mimeTypeColumnIndex);
+ final long size = cursor.getLong(sizeColumnIndex);
+ final long modified = cursor.getLong(modifiedColumnIndex);
+ new Handler(Looper.getMainLooper()).post(() -> ((FileItemViewHolder) holder).bind(ContentUris.withAppendedId(MediaStore.Files.getContentUri("external"), id), name, mimeType, size, modified, onSelect));
+ });
+ }
+ break;
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileAdapterLegacy.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileAdapterLegacy.java
new file mode 100644
index 000000000..1ac14361a
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileAdapterLegacy.java
@@ -0,0 +1,88 @@
+package it.niedermann.nextcloud.deck.ui.card.attachments.picker;
+
+import android.net.Uri;
+import android.os.Environment;
+import android.util.Pair;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.bumptech.glide.RequestBuilder;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.BiConsumer;
+
+import it.niedermann.nextcloud.deck.databinding.ItemAttachmentDefaultBinding;
+import it.niedermann.nextcloud.deck.databinding.ItemPickerNativeBinding;
+import it.niedermann.nextcloud.deck.util.AttachmentUtil;
+
+import static java.util.Collections.reverseOrder;
+import static java.util.Comparator.comparingLong;
+import static java.util.Objects.requireNonNull;
+import static java.util.stream.Collectors.toList;
+
+@Deprecated
+public class FileAdapterLegacy extends AbstractPickerAdapter<RecyclerView.ViewHolder> {
+
+ @NonNull
+ private final List<File> files;
+ @NonNull
+ protected final BiConsumer<Uri, Pair<String, RequestBuilder<?>>> onSelect;
+ @NonNull
+ protected final Runnable openNativePicker;
+
+ public FileAdapterLegacy(@NonNull BiConsumer<Uri, Pair<String, RequestBuilder<?>>> onSelect, @NonNull Runnable openNativePicker) {
+ // TODO run in separate thread?
+ this.onSelect = onSelect;
+ this.openNativePicker = openNativePicker;
+ this.files = Arrays.stream(requireNonNull(requireNonNull(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)).listFiles()))
+ .sorted(reverseOrder(comparingLong(File::lastModified)))
+ .collect(toList());
+
+ }
+
+ @NonNull
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ switch (viewType) {
+ case VIEW_TYPE_ITEM_NATIVE:
+ return new FileNativeItemViewHolder(ItemPickerNativeBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false));
+ case VIEW_TYPE_ITEM:
+ return new FileItemViewHolder(ItemAttachmentDefaultBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false));
+ default:
+ throw new IllegalStateException("Unknown viewType " + viewType);
+ }
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
+ switch (getItemViewType(position)) {
+ case VIEW_TYPE_ITEM_NATIVE: {
+ ((FileNativeItemViewHolder) holder).bind(openNativePicker);
+ break;
+ }
+ case VIEW_TYPE_ITEM: {
+ final File file = files.get(position - 1);
+ if (file.isFile()) {
+ ((FileItemViewHolder) holder).bind(Uri.fromFile(file), file.getName(), AttachmentUtil.getMimeType(file.getAbsolutePath()), file.length(), file.lastModified(), onSelect);
+ } else {
+ ((FileItemViewHolder) holder).bindError();
+ }
+ break;
+ }
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ return files.size();
+ }
+
+ public void onDestroy() {
+ // Let GarbageCollection do this stuff...
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileItemViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileItemViewHolder.java
new file mode 100644
index 000000000..f7d64aca8
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileItemViewHolder.java
@@ -0,0 +1,45 @@
+package it.niedermann.nextcloud.deck.ui.card.attachments.picker;
+
+import android.net.Uri;
+import android.util.Pair;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.bumptech.glide.RequestBuilder;
+
+import java.util.function.BiConsumer;
+
+import it.niedermann.nextcloud.deck.R;
+import it.niedermann.nextcloud.deck.databinding.ItemAttachmentDefaultBinding;
+
+import static android.text.format.Formatter.formatFileSize;
+import static it.niedermann.nextcloud.deck.util.AttachmentUtil.getIconForMimeType;
+import static it.niedermann.nextcloud.deck.util.DateUtil.getRelativeDateTimeString;
+
+public class FileItemViewHolder extends RecyclerView.ViewHolder {
+
+ private final ItemAttachmentDefaultBinding binding;
+
+ public FileItemViewHolder(@NonNull ItemAttachmentDefaultBinding binding) {
+ super(binding.getRoot());
+ this.binding = binding;
+ }
+
+ public void bind(@NonNull Uri uri, @NonNull String name, String mimeType, long size, long modified, @Nullable BiConsumer<Uri, Pair<String, RequestBuilder<?>>> onSelect) {
+ itemView.setOnClickListener(onSelect == null ? null : (v) -> onSelect.accept(uri, new Pair<>(name, null)));
+ binding.filename.setText(name);
+ binding.filesize.setText(formatFileSize(binding.filesize.getContext(), size));
+ binding.modified.setText(getRelativeDateTimeString(binding.modified.getContext(), modified));
+ binding.preview.setImageResource(getIconForMimeType(mimeType));
+ }
+
+ public void bindError() {
+ binding.filename.setText(R.string.simple_exception);
+ binding.filesize.setText(null);
+ binding.modified.setText(null);
+ itemView.setOnClickListener(null);
+ binding.preview.setImageResource(R.drawable.ic_attach_file_grey600_24dp);
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileNativeItemViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileNativeItemViewHolder.java
new file mode 100644
index 000000000..79129f26a
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/FileNativeItemViewHolder.java
@@ -0,0 +1,23 @@
+package it.niedermann.nextcloud.deck.ui.card.attachments.picker;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import it.niedermann.nextcloud.deck.R;
+import it.niedermann.nextcloud.deck.databinding.ItemPickerNativeBinding;
+
+public class FileNativeItemViewHolder extends RecyclerView.ViewHolder {
+
+ private final ItemPickerNativeBinding binding;
+
+ public FileNativeItemViewHolder(@NonNull ItemPickerNativeBinding binding) {
+ super(binding.getRoot());
+ this.binding = binding;
+ }
+
+ public void bind(Runnable onOpenMajorPicker) {
+ binding.title.setText(R.string.show_all_files);
+ binding.subtitle.setText(R.string.downloads);
+ itemView.setOnClickListener((v) -> onOpenMajorPicker.run());
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryAdapter.java
new file mode 100644
index 000000000..658eb1ee3
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryAdapter.java
@@ -0,0 +1,100 @@
+package it.niedermann.nextcloud.deck.ui.card.attachments.picker;
+
+import android.annotation.SuppressLint;
+import android.content.ContentUris;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.provider.MediaStore;
+import android.util.Pair;
+import android.util.Size;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.bumptech.glide.RequestBuilder;
+
+import java.io.IOException;
+import java.util.function.BiConsumer;
+
+import it.niedermann.nextcloud.deck.databinding.ItemAttachmentImageBinding;
+import it.niedermann.nextcloud.deck.databinding.ItemPhotoPreviewBinding;
+
+import static android.os.Build.VERSION.SDK_INT;
+import static android.os.Build.VERSION_CODES.Q;
+import static android.provider.BaseColumns._ID;
+import static android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
+
+public class GalleryAdapter extends AbstractCursorPickerAdapter<RecyclerView.ViewHolder> {
+
+ @NonNull
+ private final LifecycleOwner lifecycleOwner;
+
+ @SuppressLint("InlinedApi")
+ private static final String sortOrder = (SDK_INT >= Q)
+ ? MediaStore.Images.Media.DATE_TAKEN
+ : MediaStore.Images.Media.DATE_ADDED;
+
+ public GalleryAdapter(@NonNull Context context, @NonNull BiConsumer<Uri, Pair<String, RequestBuilder<?>>> onSelect, @NonNull Runnable openNativePicker, @NonNull LifecycleOwner lifecycleOwner) {
+ super(context, onSelect, openNativePicker, EXTERNAL_CONTENT_URI, _ID, sortOrder + " DESC");
+ this.lifecycleOwner = lifecycleOwner;
+ notifyItemRangeInserted(0, getItemCount() + 1);
+ }
+
+ @NonNull
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ switch (viewType) {
+ case VIEW_TYPE_ITEM_NATIVE:
+ return new GalleryPhotoPreviewItemViewHolder(ItemPhotoPreviewBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false));
+ case VIEW_TYPE_ITEM:
+ return new GalleryItemViewHolder(ItemAttachmentImageBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false));
+ default:
+ throw new IllegalStateException("Unknown viewType " + viewType);
+ }
+
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
+ switch (getItemViewType(position)) {
+ case VIEW_TYPE_ITEM_NATIVE: {
+ ((GalleryPhotoPreviewItemViewHolder) holder).bind(openNativePicker, lifecycleOwner);
+ break;
+ }
+ case VIEW_TYPE_ITEM: {
+ final long id = getItemId(position);
+ bindExecutor.execute(() -> {
+ try {
+ final Bitmap thumbnail;
+ if (SDK_INT >= Q) {
+ thumbnail = contentResolver.loadThumbnail(ContentUris.withAppendedId(
+ EXTERNAL_CONTENT_URI, id), new Size(512, 384), null);
+ } else {
+ thumbnail = MediaStore.Images.Thumbnails.getThumbnail(
+ contentResolver, id,
+ MediaStore.Images.Thumbnails.MINI_KIND, null);
+ }
+ new Handler(Looper.getMainLooper()).post(() -> ((GalleryItemViewHolder) holder).bind(ContentUris.withAppendedId(
+ EXTERNAL_CONTENT_URI, id), thumbnail, onSelect));
+ } catch (IOException ignored) {
+ new Handler(Looper.getMainLooper()).post(((GalleryItemViewHolder) holder)::bindError);
+ }
+ });
+ }
+ }
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(@NonNull RecyclerView.ViewHolder holder) {
+ super.onViewDetachedFromWindow(holder);
+ if (holder instanceof GalleryPhotoPreviewItemViewHolder) {
+ ((GalleryPhotoPreviewItemViewHolder) holder).unbind();
+ }
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryItemDecoration.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryItemDecoration.java
new file mode 100644
index 000000000..c70dc8277
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryItemDecoration.java
@@ -0,0 +1,29 @@
+package it.niedermann.nextcloud.deck.ui.card.attachments.picker;
+
+import android.graphics.Rect;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Px;
+import androidx.recyclerview.widget.RecyclerView;
+
+public class GalleryItemDecoration extends RecyclerView.ItemDecoration {
+
+ @Px
+ private final int gutter;
+
+ public GalleryItemDecoration(@Px int gutter) {
+ this.gutter = gutter;
+ }
+
+ @Override
+ public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
+ final int position = parent.getChildAdapterPosition(view);
+ if (position >= 0) {
+ outRect.left = gutter;
+ outRect.top = gutter;
+ outRect.right = gutter;
+ outRect.bottom = gutter;
+ }
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryItemViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryItemViewHolder.java
new file mode 100644
index 000000000..346fca9c3
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryItemViewHolder.java
@@ -0,0 +1,42 @@
+package it.niedermann.nextcloud.deck.ui.card.attachments.picker;
+
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.util.Pair;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.RequestBuilder;
+
+import java.util.function.BiConsumer;
+
+import it.niedermann.nextcloud.deck.R;
+import it.niedermann.nextcloud.deck.databinding.ItemAttachmentImageBinding;
+
+public class GalleryItemViewHolder extends RecyclerView.ViewHolder {
+
+ private final ItemAttachmentImageBinding binding;
+
+ public GalleryItemViewHolder(@NonNull ItemAttachmentImageBinding binding) {
+ super(binding.getRoot());
+ this.binding = binding;
+ }
+
+ public void bind(@NonNull Uri uri, @Nullable Bitmap image, @NonNull BiConsumer<Uri, Pair<String, RequestBuilder<?>>> onSelect) {
+ itemView.setOnClickListener((v) -> onSelect.accept(uri, new Pair<>(null, Glide.with(itemView.getContext()).load(image))));
+ Glide.with(itemView.getContext())
+ .load(image)
+ .placeholder(R.drawable.ic_image_grey600_24dp)
+ .into(binding.preview);
+ }
+
+ public void bindError() {
+ itemView.setOnClickListener(null);
+ Glide.with(itemView.getContext())
+ .load(R.drawable.ic_image_grey600_24dp)
+ .into(binding.preview);
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryPhotoPreviewItemViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryPhotoPreviewItemViewHolder.java
new file mode 100644
index 000000000..00a833e57
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryPhotoPreviewItemViewHolder.java
@@ -0,0 +1,51 @@
+package it.niedermann.nextcloud.deck.ui.card.attachments.picker;
+
+import androidx.annotation.NonNull;
+import androidx.camera.core.Preview;
+import androidx.camera.lifecycle.ProcessCameraProvider;
+import androidx.core.content.ContextCompat;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.concurrent.ExecutionException;
+
+import it.niedermann.nextcloud.deck.DeckLog;
+import it.niedermann.nextcloud.deck.databinding.ItemPhotoPreviewBinding;
+
+import static androidx.camera.core.CameraSelector.DEFAULT_BACK_CAMERA;
+
+public class GalleryPhotoPreviewItemViewHolder extends RecyclerView.ViewHolder {
+
+ private final ItemPhotoPreviewBinding binding;
+ private ProcessCameraProvider cameraProvider;
+
+ public GalleryPhotoPreviewItemViewHolder(@NonNull ItemPhotoPreviewBinding binding) {
+ super(binding.getRoot());
+ this.binding = binding;
+ }
+
+ public void bind(@NonNull Runnable openNativePicker, @NonNull LifecycleOwner lifecycleOwner) {
+ itemView.setOnClickListener((v) -> openNativePicker.run());
+ ListenableFuture<ProcessCameraProvider> cameraProviderFuture = ProcessCameraProvider.getInstance(itemView.getContext());
+ cameraProviderFuture.addListener(() -> {
+ try {
+ unbind();
+ cameraProvider = cameraProviderFuture.get();
+ Preview previewUseCase = new Preview.Builder().build();
+ previewUseCase.setSurfaceProvider(binding.preview.getSurfaceProvider());
+ cameraProvider.bindToLifecycle(lifecycleOwner, DEFAULT_BACK_CAMERA, previewUseCase);
+ } catch (ExecutionException | InterruptedException | IllegalArgumentException e) {
+ DeckLog.logError(e);
+ }
+ }, ContextCompat.getMainExecutor(itemView.getContext()));
+ }
+
+
+ public void unbind() {
+ if (cameraProvider != null) {
+ cameraProvider.unbindAll();
+ }
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/previewdialog/PreviewDialog.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/previewdialog/PreviewDialog.java
new file mode 100644
index 000000000..8ebdf1b50
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/previewdialog/PreviewDialog.java
@@ -0,0 +1,102 @@
+package it.niedermann.nextcloud.deck.ui.card.attachments.previewdialog;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.graphics.Color;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+
+import androidx.annotation.NonNull;
+import androidx.fragment.app.DialogFragment;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.swiperefreshlayout.widget.CircularProgressDrawable;
+
+import com.bumptech.glide.RequestBuilder;
+
+import it.niedermann.nextcloud.deck.R;
+import it.niedermann.nextcloud.deck.databinding.DialogPreviewBinding;
+import it.niedermann.nextcloud.deck.ui.branding.BrandedAlertDialogBuilder;
+import it.niedermann.nextcloud.deck.ui.branding.BrandedDialogFragment;
+
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+import static it.niedermann.nextcloud.deck.DeckApplication.isDarkTheme;
+
+public class PreviewDialog extends BrandedDialogFragment {
+
+ private DialogPreviewBinding binding;
+ private PreviewDialogViewModel viewModel;
+ private LiveData<RequestBuilder<?>> imageBuilder$;
+ private LiveData<String> title$;
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ viewModel = new ViewModelProvider(requireActivity()).get(PreviewDialogViewModel.class);
+ binding = DialogPreviewBinding.inflate(LayoutInflater.from(requireContext()));
+
+ final Context context = requireContext();
+
+ this.imageBuilder$ = this.viewModel.getImageBuilder();
+ this.imageBuilder$.observe(requireActivity(), builder -> {
+ if (builder == null) {
+ binding.avatar.setVisibility(GONE);
+ } else {
+ final CircularProgressDrawable circularProgressDrawable = new CircularProgressDrawable(context);
+ circularProgressDrawable.setStrokeWidth(5f);
+ circularProgressDrawable.setCenterRadius(30f);
+ circularProgressDrawable.setColorSchemeColors(isDarkTheme(context) ? Color.LTGRAY : Color.DKGRAY);
+ circularProgressDrawable.start();
+ binding.avatar.setVisibility(VISIBLE);
+ binding.avatar.post(() -> builder
+ .placeholder(circularProgressDrawable)
+ .into(binding.avatar));
+ }
+ });
+ this.title$ = this.viewModel.getTitle();
+ this.title$.observe(requireActivity(), title -> {
+ if (TextUtils.isEmpty(title)) {
+ binding.title.setVisibility(GONE);
+ } else {
+ binding.title.setVisibility(VISIBLE);
+ binding.title.setText(title);
+ }
+ });
+
+ return new BrandedAlertDialogBuilder(requireContext())
+ .setPositiveButton(R.string.simple_attach, (d, w) -> {
+ viewModel.setResult(true);
+ dismiss();
+ })
+ .setNeutralButton(R.string.simple_close, (d, w) -> {
+ viewModel.setResult(false);
+ dismiss();
+ })
+ .setView(binding.getRoot())
+ .create();
+ }
+
+ @Override
+ public void onCancel(@NonNull DialogInterface dialog) {
+ viewModel.setResult(false);
+ super.onCancel(dialog);
+ }
+
+ @Override
+ public void applyBrand(int mainColor) {
+ }
+
+ @Override
+ public void onDestroy() {
+ this.imageBuilder$.removeObservers(requireActivity());
+ this.title$.removeObservers(requireActivity());
+ super.onDestroy();
+ }
+
+ public static DialogFragment newInstance() {
+ return new PreviewDialog();
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/previewdialog/PreviewDialogViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/previewdialog/PreviewDialogViewModel.java
new file mode 100644
index 000000000..8ee8a0e08
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/previewdialog/PreviewDialogViewModel.java
@@ -0,0 +1,50 @@
+package it.niedermann.nextcloud.deck.ui.card.attachments.previewdialog;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.ViewModel;
+
+import com.bumptech.glide.RequestBuilder;
+
+import static androidx.lifecycle.Transformations.distinctUntilChanged;
+
+public class PreviewDialogViewModel extends ViewModel {
+
+ @NonNull
+ private final MutableLiveData<String> title$ = new MutableLiveData<>();
+ @NonNull
+ private final MutableLiveData<RequestBuilder<?>> imageBuilder$ = new MutableLiveData<>();
+ private MutableLiveData<Boolean> result$ = new MutableLiveData<>();
+
+ /**
+ * Call this before observing {@link #getResult()} to prepare the {@link PreviewDialog}.
+ */
+ public void prepareDialog(@Nullable String title, @Nullable RequestBuilder<?> imageBuilder) {
+ this.result$ = new MutableLiveData<>();
+ this.title$.setValue(title);
+ this.imageBuilder$.setValue(imageBuilder);
+ }
+
+ /**
+ * This will be a new instance after each call of {@link #prepareDialog(String, RequestBuilder)}.
+ *
+ * @return {@link Boolean#TRUE} if a positive action has been submitted, {@link Boolean#FALSE} if the dialog has been canceled.
+ */
+ public LiveData<Boolean> getResult() {
+ return this.result$;
+ }
+
+ protected LiveData<String> getTitle() {
+ return distinctUntilChanged(this.title$);
+ }
+
+ protected LiveData<RequestBuilder<?>> getImageBuilder() {
+ return distinctUntilChanged(this.imageBuilder$);
+ }
+
+ protected void setResult(boolean submittedPositive) {
+ result$.setValue(submittedPositive);
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserAdapter.java
index 82230060c..4b75b985f 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserAdapter.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserAdapter.java
@@ -84,14 +84,14 @@ public class FilterUserAdapter extends RecyclerView.Adapter<FilterUserAdapter.Us
}
void bind(@NonNull final User user) {
- binding.displayName.setText(user.getDisplayname());
+ binding.title.setText(user.getDisplayname());
ViewUtil.addAvatar(binding.avatar, account.getUrl(), user.getUid(), avatarSize, R.drawable.ic_person_grey600_24dp);
itemView.setSelected(selectedUsers.contains(user));
bindClickListener(user);
}
public void bindNotAssigned() {
- binding.displayName.setText(itemView.getContext().getString(R.string.simple_unassigned));
+ binding.title.setText(itemView.getContext().getString(R.string.simple_unassigned));
Glide.with(itemView.getContext())
.load(R.drawable.ic_baseline_block_24)
.into(binding.avatar);
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/takephoto/TakePhotoActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/takephoto/TakePhotoActivity.java
new file mode 100644
index 000000000..af17464dc
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/takephoto/TakePhotoActivity.java
@@ -0,0 +1,182 @@
+package it.niedermann.nextcloud.deck.ui.takephoto;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.ColorStateList;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Size;
+import android.view.OrientationEventListener;
+import android.view.Surface;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.Camera;
+import androidx.camera.core.ImageCapture;
+import androidx.camera.core.ImageCaptureException;
+import androidx.camera.core.Preview;
+import androidx.camera.lifecycle.ProcessCameraProvider;
+import androidx.core.content.ContextCompat;
+import androidx.lifecycle.ViewModelProvider;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.io.File;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.concurrent.ExecutionException;
+
+import it.niedermann.nextcloud.deck.DeckLog;
+import it.niedermann.nextcloud.deck.databinding.ActivityTakePhotoBinding;
+import it.niedermann.nextcloud.deck.ui.branding.BrandedActivity;
+import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment;
+import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler;
+import it.niedermann.nextcloud.deck.util.AttachmentUtil;
+
+import static android.os.Build.VERSION_CODES.LOLLIPOP;
+import static it.niedermann.nextcloud.deck.util.MimeTypeUtil.IMAGE_JPEG;
+
+@RequiresApi(LOLLIPOP)
+public class TakePhotoActivity extends BrandedActivity {
+
+ private ActivityTakePhotoBinding binding;
+ private TakePhotoViewModel viewModel;
+
+ private View[] brandedViews;
+
+ private ListenableFuture<ProcessCameraProvider> cameraProviderFuture;
+ private OrientationEventListener orientationEventListener;
+
+ private final DateTimeFormatter fileNameFromCameraFormatter = DateTimeFormatter.ofPattern("'JPG_'yyyyMMdd'_'HHmmss'.jpg'");
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ Thread.currentThread().setUncaughtExceptionHandler(new ExceptionHandler(this));
+
+ binding = ActivityTakePhotoBinding.inflate(getLayoutInflater());
+ viewModel = new ViewModelProvider(this).get(TakePhotoViewModel.class);
+
+ setContentView(binding.getRoot());
+
+ cameraProviderFuture = ProcessCameraProvider.getInstance(this);
+ cameraProviderFuture.addListener(() -> {
+ try {
+ final ProcessCameraProvider cameraProvider = cameraProviderFuture.get();
+ final Preview previewUseCase = getPreviewUseCase();
+ final ImageCapture captureUseCase = getCaptureUseCase();
+ final Camera camera = cameraProvider.bindToLifecycle(this, viewModel.getCameraSelector(), captureUseCase, previewUseCase);
+
+ viewModel.getCameraSelectorToggleButtonImageResource().observe(this, res -> binding.switchCamera.setImageDrawable(ContextCompat.getDrawable(this, res)));
+ viewModel.getTorchToggleButtonImageResource().observe(this, res -> binding.toggleTorch.setImageDrawable(ContextCompat.getDrawable(this, res)));
+ viewModel.isTorchEnabled().observe(this, enabled -> camera.getCameraControl().enableTorch(enabled));
+
+ binding.toggleTorch.setOnClickListener((v) -> viewModel.toggleTorchEnabled());
+ binding.switchCamera.setOnClickListener((v) -> {
+ viewModel.toggleCameraSelector();
+ cameraProvider.unbindAll();
+ cameraProvider.bindToLifecycle(this, viewModel.getCameraSelector(), captureUseCase, previewUseCase);
+ });
+ } catch (ExecutionException | InterruptedException e) {
+ DeckLog.logError(e);
+ finish();
+ }
+ }, ContextCompat.getMainExecutor(this));
+
+ brandedViews = new View[]{binding.takePhoto, binding.switchCamera, binding.toggleTorch};
+ }
+
+ private ImageCapture getCaptureUseCase() {
+ final ImageCapture captureUseCase = new ImageCapture.Builder().setTargetResolution(new Size(720, 1280)).build();
+
+ orientationEventListener = new OrientationEventListener(this) {
+ @Override
+ public void onOrientationChanged(int orientation) {
+ int rotation;
+
+ // Monitors orientation values to determine the target rotation value
+ if (orientation >= 45 && orientation < 135) {
+ rotation = Surface.ROTATION_270;
+ } else if (orientation >= 135 && orientation < 225) {
+ rotation = Surface.ROTATION_180;
+ } else if (orientation >= 225 && orientation < 315) {
+ rotation = Surface.ROTATION_90;
+ } else {
+ rotation = Surface.ROTATION_0;
+ }
+
+ captureUseCase.setTargetRotation(rotation);
+ }
+ };
+ orientationEventListener.enable();
+
+ binding.takePhoto.setOnClickListener((v) -> {
+ binding.takePhoto.setEnabled(false);
+ final String photoFileName = Instant.now().atZone(ZoneId.systemDefault()).format(fileNameFromCameraFormatter);
+ try {
+ final File photoFile = AttachmentUtil.getTempCacheFile(this, "photos/" + photoFileName);
+ final ImageCapture.OutputFileOptions options = new ImageCapture.OutputFileOptions.Builder(photoFile).build();
+ captureUseCase.takePicture(options, ContextCompat.getMainExecutor(this), new ImageCapture.OnImageSavedCallback() {
+ @Override
+ public void onImageSaved(@NonNull ImageCapture.OutputFileResults outputFileResults) {
+ final Uri savedUri = Uri.fromFile(photoFile);
+ DeckLog.info("onImageSaved - savedUri: " + savedUri.toString());
+ setResult(RESULT_OK, new Intent().setDataAndType(savedUri, IMAGE_JPEG));
+ finish();
+ }
+
+ @Override
+ public void onError(@NonNull ImageCaptureException e) {
+ e.printStackTrace();
+ //noinspection ResultOfMethodCallIgnored
+ photoFile.delete();
+ binding.takePhoto.setEnabled(true);
+ }
+ });
+ } catch (Exception e) {
+ ExceptionDialogFragment.newInstance(e, null).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ }
+ });
+
+ return captureUseCase;
+ }
+
+ private Preview getPreviewUseCase() {
+ Preview previewUseCase = new Preview.Builder().build();
+ previewUseCase.setSurfaceProvider(binding.preview.getSurfaceProvider());
+ return previewUseCase;
+ }
+
+ @Override
+ protected void onPause() {
+ if (this.orientationEventListener != null) {
+ this.orientationEventListener.disable();
+ }
+ super.onPause();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ if (this.orientationEventListener != null) {
+ this.orientationEventListener.enable();
+ }
+ }
+
+ @RequiresApi(LOLLIPOP)
+ public static Intent createIntent(@NonNull Context context) {
+ return new Intent(context, TakePhotoActivity.class).setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
+ }
+
+ @Override
+ public void applyBrand(int mainColor) {
+ final ColorStateList colorStateList = ColorStateList.valueOf(mainColor);
+ for (View v : brandedViews) {
+ v.setBackgroundTintList(colorStateList);
+ }
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/takephoto/TakePhotoViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/takephoto/TakePhotoViewModel.java
new file mode 100644
index 000000000..a71291ff2
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/takephoto/TakePhotoViewModel.java
@@ -0,0 +1,57 @@
+package it.niedermann.nextcloud.deck.ui.takephoto;
+
+import androidx.annotation.NonNull;
+import androidx.camera.core.CameraSelector;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.Transformations;
+import androidx.lifecycle.ViewModel;
+
+import it.niedermann.nextcloud.deck.R;
+
+import static androidx.camera.core.CameraSelector.DEFAULT_BACK_CAMERA;
+import static androidx.camera.core.CameraSelector.DEFAULT_FRONT_CAMERA;
+
+public class TakePhotoViewModel extends ViewModel {
+
+ @NonNull
+ private CameraSelector cameraSelector = DEFAULT_BACK_CAMERA;
+ @NonNull
+ private final MutableLiveData<Integer> cameraSelectorToggleButtonImageResource = new MutableLiveData<>(R.drawable.ic_baseline_camera_front_24);
+ @NonNull
+ private final MutableLiveData<Boolean> torchEnabled = new MutableLiveData<>(false);
+
+ @NonNull
+ public CameraSelector getCameraSelector() {
+ return this.cameraSelector;
+ }
+
+ public LiveData<Integer> getCameraSelectorToggleButtonImageResource() {
+ return this.cameraSelectorToggleButtonImageResource;
+ }
+
+ public void toggleCameraSelector() {
+ if (this.cameraSelector == DEFAULT_BACK_CAMERA) {
+ this.cameraSelector = DEFAULT_FRONT_CAMERA;
+ this.cameraSelectorToggleButtonImageResource.postValue(R.drawable.ic_baseline_camera_rear_24);
+ } else {
+ this.cameraSelector = DEFAULT_BACK_CAMERA;
+ this.cameraSelectorToggleButtonImageResource.postValue(R.drawable.ic_baseline_camera_front_24);
+ }
+ }
+
+ public void toggleTorchEnabled() {
+ //noinspection ConstantConditions
+ this.torchEnabled.postValue(!this.torchEnabled.getValue());
+ }
+
+ public LiveData<Boolean> isTorchEnabled() {
+ return this.torchEnabled;
+ }
+
+ public LiveData<Integer> getTorchToggleButtonImageResource() {
+ return Transformations.map(isTorchEnabled(), enabled -> enabled
+ ? R.drawable.ic_baseline_flash_off_24
+ : R.drawable.ic_baseline_flash_on_24);
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/SquareConstraintLayout.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/SquareConstraintLayout.java
new file mode 100644
index 000000000..0912a07dd
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/SquareConstraintLayout.java
@@ -0,0 +1,35 @@
+package it.niedermann.nextcloud.deck.ui.view;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Build;
+import android.util.AttributeSet;
+
+import androidx.constraintlayout.widget.ConstraintLayout;
+
+public class SquareConstraintLayout extends ConstraintLayout {
+
+ public SquareConstraintLayout(Context context) {
+ super(context);
+ }
+
+ public SquareConstraintLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public SquareConstraintLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ public SquareConstraintLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ // Set a square layout.
+ super.onMeasure(widthMeasureSpec, widthMeasureSpec);
+ }
+
+} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/AttachmentUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/AttachmentUtil.java
index 844a301e8..2baec8e92 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/util/AttachmentUtil.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/util/AttachmentUtil.java
@@ -3,8 +3,11 @@ package it.niedermann.nextcloud.deck.util;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
+import android.text.TextUtils;
+import android.webkit.MimeTypeMap;
import android.widget.Toast;
+import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -58,34 +61,78 @@ public class AttachmentUtil {
return accountUrl + "/index.php/apps/deck/cards/" + cardRemoteId + "/attachment/" + attachmentRemoteId;
}
- public static File copyContentUriToTempFile(@NonNull Context context, @NonNull Uri currentUri, long accountId, Long localId) throws IOException, IllegalArgumentException {
- String fullTempPath = context.getApplicationContext().getFilesDir().getAbsolutePath() + "/attachments/account-" + accountId + "/card-" + (localId == null ? "pending-creation" : localId) + '/' + UriUtils.getDisplayNameForUri(currentUri, context);
- DeckLog.verbose("----- fullTempPath: " + fullTempPath);
- InputStream inputStream = context.getContentResolver().openInputStream(currentUri);
+ public static File copyContentUriToTempFile(@NonNull Context context, @NonNull Uri currentUri, long accountId, Long localCardId) throws IOException, IllegalArgumentException {
+ final InputStream inputStream = context.getContentResolver().openInputStream(currentUri);
if (inputStream == null) {
throw new IOException("Could not open input stream for " + currentUri.getPath());
}
- File cacheFile = new File(fullTempPath);
- File tempDir = cacheFile.getParentFile();
+ final File cacheFile = getTempCacheFile(context, "attachments/account-" + accountId + "/card-" + (localCardId == null ? "pending-creation" : localCardId) + '/' + UriUtils.getDisplayNameForUri(currentUri, context));
+ final FileOutputStream outputStream = new FileOutputStream(cacheFile);
+ byte[] buffer = new byte[4096];
+
+ int count;
+ while ((count = inputStream.read(buffer)) > 0) {
+ outputStream.write(buffer, 0, count);
+ }
+ DeckLog.verbose("----- wrote");
+ return cacheFile;
+ }
+
+ /**
+ * Creates a new {@link File}
+ */
+ public static File getTempCacheFile(@NonNull Context context, String fileName) throws IOException {
+ File cacheFile = new File(context.getApplicationContext().getFilesDir().getAbsolutePath() + "/" + fileName);
+
+ DeckLog.verbose("- Full path for new cache file: " + cacheFile.getAbsolutePath());
+
+ final File tempDir = cacheFile.getParentFile();
if (tempDir == null) {
- throw new FileNotFoundException("could not cacheFile.getPranetFile()");
+ throw new FileNotFoundException("could not cacheFile.getParentFile()");
}
if (!tempDir.exists()) {
- if (!tempDir.mkdirs()) {
+ DeckLog.verbose("-- The folder in which the new file should be created does not exist yet. Trying to create it...");
+ if (tempDir.mkdirs()) {
+ DeckLog.verbose("--- Creation successful");
+ } else {
throw new IOException("Directory for temporary file does not exist and could not be created.");
}
}
- if (!cacheFile.createNewFile()) {
+
+ DeckLog.verbose("- Try to create actual cache file");
+ if (cacheFile.createNewFile()) {
+ DeckLog.verbose("-- Successfully created cache file");
+ } else {
throw new IOException("Failed to create cacheFile");
}
- FileOutputStream outputStream = new FileOutputStream(fullTempPath);
- byte[] buffer = new byte[4096];
- int count;
- while ((count = inputStream.read(buffer)) > 0) {
- outputStream.write(buffer, 0, count);
- }
- DeckLog.verbose("----- wrote");
return cacheFile;
}
+
+ @DrawableRes
+ public static int getIconForMimeType(@NonNull String mimeType) {
+ if (TextUtils.isEmpty(mimeType)) {
+ return R.drawable.ic_attach_file_grey600_24dp;
+ } else if (MimeTypeUtil.isAudio(mimeType)) {
+ return R.drawable.ic_music_note_grey600_24dp;
+ } else if (MimeTypeUtil.isVideo(mimeType)) {
+ return R.drawable.ic_local_movies_grey600_24dp;
+ } else if (MimeTypeUtil.isPdf(mimeType)) {
+ return R.drawable.ic_baseline_picture_as_pdf_24;
+ } else if (MimeTypeUtil.isContact(mimeType)) {
+ return R.drawable.ic_baseline_contact_mail_24;
+ } else {
+ return R.drawable.ic_attach_file_grey600_24dp;
+ }
+ }
+
+ public static String getMimeType(@Nullable String url) {
+ String type = null;
+ String extension = MimeTypeMap.getFileExtensionFromUrl(url);
+ if (extension != null) {
+ type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
+ }
+ return type;
+ }
+
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/MimeTypeUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/MimeTypeUtil.java
index 0390bf96d..04694a058 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/util/MimeTypeUtil.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/util/MimeTypeUtil.java
@@ -6,6 +6,7 @@ import java.util.Locale;
public class MimeTypeUtil {
+ public static final String IMAGE_JPEG = "image/jpeg";
public static final String TEXT_PLAIN = "text/plain";
public static final String TEXT_VCARD = "text/vcard";
public static final String APPLICATION_PDF = "application/pdf";
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/VCardUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/VCardUtil.java
new file mode 100644
index 000000000..274af332d
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/util/VCardUtil.java
@@ -0,0 +1,42 @@
+package it.niedermann.nextcloud.deck.util;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.Color;
+import android.net.Uri;
+import android.provider.ContactsContract;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+
+import java.util.NoSuchElementException;
+import java.util.Objects;
+
+import it.niedermann.nextcloud.deck.R;
+
+public class VCardUtil {
+
+ private VCardUtil() {
+ // You shall not pass
+ }
+
+ public static Uri getVCardContentUri(@NonNull Context context, @NonNull Uri contactUri) throws NoSuchElementException {
+ final ContentResolver cr = context.getContentResolver();
+ try (final Cursor cursor = cr.query(contactUri, null, null, null, null)) {
+ if (cursor != null && cursor.moveToFirst()) {
+ final String lookupKey = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts.LOOKUP_KEY));
+ return Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_VCARD_URI, lookupKey);
+ } else {
+ throw new NoSuchElementException("Cursor has zero entries");
+ }
+ }
+ }
+
+ @ColorInt
+ public static int getColorBasedOnDisplayName(@NonNull Context context, @NonNull String displayName) {
+ final String[] colors = context.getResources().getStringArray(R.array.board_default_colors);
+ final int hashCode = Objects.hashCode(displayName);
+ return Color.parseColor(colors[(hashCode < 0 ? hashCode * -1 : hashCode) % colors.length]);
+ }
+}
diff --git a/app/src/main/res/drawable-v21/bottom_sheet_rounded.xml b/app/src/main/res/drawable-v21/bottom_sheet_rounded.xml
new file mode 100644
index 000000000..ba266ed32
--- /dev/null
+++ b/app/src/main/res/drawable-v21/bottom_sheet_rounded.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <solid android:color="?attr/colorSurface" />
+ <corners
+ android:topLeftRadius="16dp"
+ android:topRightRadius="16dp" />
+
+</shape> \ No newline at end of file
diff --git a/app/src/main/res/drawable-xxxhdpi/background.png b/app/src/main/res/drawable-xxxhdpi/background.png
deleted file mode 100644
index 90856f4c8..000000000
--- a/app/src/main/res/drawable-xxxhdpi/background.png
+++ /dev/null
Binary files differ
diff --git a/app/src/main/res/drawable/bottom_sheet_rounded.xml b/app/src/main/res/drawable/bottom_sheet_rounded.xml
new file mode 100644
index 000000000..cef4c2314
--- /dev/null
+++ b/app/src/main/res/drawable/bottom_sheet_rounded.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <solid android:color="@color/primary" />
+ <corners
+ android:topLeftRadius="16dp"
+ android:topRightRadius="16dp" />
+</shape> \ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_baseline_camera_front_24.xml b/app/src/main/res/drawable/ic_baseline_camera_front_24.xml
new file mode 100644
index 000000000..25c1a79b8
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_camera_front_24.xml
@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#757575"
+ android:viewportHeight="24" android:viewportWidth="24"
+ android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="@android:color/white" android:pathData="M10,20L5,20v2h5v2l3,-3 -3,-3v2zM14,20v2h5v-2h-5zM12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -1.99,0.9 -1.99,2S10.9,8 12,8zM17,0L7,0C5.9,0 5,0.9 5,2v14c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2L19,2c0,-1.1 -0.9,-2 -2,-2zM7,2h10v10.5c0,-1.67 -3.33,-2.5 -5,-2.5s-5,0.83 -5,2.5L7,2z"/>
+</vector>
diff --git a/app/src/main/res/drawable/ic_baseline_camera_rear_24.xml b/app/src/main/res/drawable/ic_baseline_camera_rear_24.xml
new file mode 100644
index 000000000..51cea2177
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_camera_rear_24.xml
@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#757575"
+ android:viewportHeight="24" android:viewportWidth="24"
+ android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="@android:color/white" android:pathData="M10,20L5,20v2h5v2l3,-3 -3,-3v2zM14,20v2h5v-2h-5zM17,0L7,0C5.9,0 5,0.9 5,2v14c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2L19,2c0,-1.1 -0.9,-2 -2,-2zM12,6c-1.11,0 -2,-0.9 -2,-2s0.89,-2 1.99,-2 2,0.9 2,2C14,5.1 13.1,6 12,6z"/>
+</vector>
diff --git a/app/src/main/res/drawable/ic_baseline_flash_off_24.xml b/app/src/main/res/drawable/ic_baseline_flash_off_24.xml
new file mode 100644
index 000000000..2a3b0ff5d
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_flash_off_24.xml
@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#757575"
+ android:viewportHeight="24" android:viewportWidth="24"
+ android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="@android:color/white" android:pathData="M3.27,3L2,4.27l5,5V13h3v9l3.58,-6.14L17.73,20 19,18.73 3.27,3zM17,10h-4l4,-8H7v2.18l8.46,8.46L17,10z"/>
+</vector>
diff --git a/app/src/main/res/drawable/ic_baseline_flash_on_24.xml b/app/src/main/res/drawable/ic_baseline_flash_on_24.xml
new file mode 100644
index 000000000..4574d0e20
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_flash_on_24.xml
@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#757575"
+ android:viewportHeight="24" android:viewportWidth="24"
+ android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="@android:color/white" android:pathData="M7,2v11h3v9l7,-12h-4l4,-8z"/>
+</vector>
diff --git a/app/src/main/res/drawable/ic_baseline_photo_camera_24.xml b/app/src/main/res/drawable/ic_baseline_photo_camera_24.xml
new file mode 100644
index 000000000..497db8383
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_photo_camera_24.xml
@@ -0,0 +1,6 @@
+<vector android:autoMirrored="true" android:height="24dp"
+ android:tint="#757575" android:viewportHeight="24"
+ android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="@android:color/white" android:pathData="M12,12m-3.2,0a3.2,3.2 0,1 1,6.4 0a3.2,3.2 0,1 1,-6.4 0"/>
+ <path android:fillColor="@android:color/white" android:pathData="M9,2L7.17,4L4,4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2h-3.17L15,2L9,2zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5z"/>
+</vector>
diff --git a/app/src/main/res/drawable/ic_baseline_search_24.xml b/app/src/main/res/drawable/ic_baseline_search_24.xml
new file mode 100644
index 000000000..2eb5033c1
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_search_24.xml
@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#757575"
+ android:viewportHeight="24" android:viewportWidth="24"
+ android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+ <path android:fillColor="@android:color/white" android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z"/>
+</vector>
diff --git a/app/src/main/res/layout/activity_take_photo.xml b/app/src/main/res/layout/activity_take_photo.xml
new file mode 100644
index 000000000..419bb54f7
--- /dev/null
+++ b/app/src/main/res/layout/activity_take_photo.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@android:color/black"
+ android:orientation="vertical"
+ tools:theme="@style/TransparentTheme">
+
+ <androidx.camera.view.PreviewView
+ android:id="@+id/preview"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+ <com.google.android.flexbox.FlexboxLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:background="@color/mdtp_transparent_black"
+ app:alignItems="center"
+ app:justifyContent="space_evenly">
+
+ <com.google.android.material.floatingactionbutton.FloatingActionButton
+ android:id="@+id/switchCamera"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:contentDescription="@string/take_photo_switch_camera"
+ android:tint="@android:color/white"
+ app:backgroundTint="@color/defaultBrand"
+ app:fabSize="mini"
+ tools:srcCompat="@drawable/ic_baseline_camera_front_24" />
+
+ <com.google.android.material.floatingactionbutton.FloatingActionButton
+ android:id="@+id/takePhoto"
+ android:layout_marginTop="@dimen/spacer_3x"
+ android:layout_marginBottom="@dimen/spacer_3x"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:contentDescription="@string/take_photo"
+ android:tint="@android:color/white"
+ app:backgroundTint="@color/defaultBrand"
+ app:srcCompat="@drawable/ic_baseline_photo_camera_24" />
+
+ <com.google.android.material.floatingactionbutton.FloatingActionButton
+ android:id="@+id/toggle_torch"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:contentDescription="@string/take_photo_toggle_torch"
+ android:tint="@android:color/white"
+ app:backgroundTint="@color/defaultBrand"
+ app:fabSize="mini"
+ tools:srcCompat="@drawable/ic_baseline_flash_on_24" />
+ </com.google.android.flexbox.FlexboxLayout>
+</RelativeLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/dialog_assignee.xml b/app/src/main/res/layout/dialog_preview.xml
index 6f7157286..5ab35d63e 100644
--- a/app/src/main/res/layout/dialog_assignee.xml
+++ b/app/src/main/res/layout/dialog_preview.xml
@@ -15,7 +15,7 @@
<TextView
android:padding="?dialogPreferredPadding"
- android:id="@+id/displayName"
+ android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceHeadline1"
diff --git a/app/src/main/res/layout/fragment_card_edit_tab_attachments.xml b/app/src/main/res/layout/fragment_card_edit_tab_attachments.xml
index 5aa23961d..c1cccc310 100644
--- a/app/src/main/res/layout/fragment_card_edit_tab_attachments.xml
+++ b/app/src/main/res/layout/fragment_card_edit_tab_attachments.xml
@@ -31,8 +31,67 @@
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/fab_margin"
+ android:contentDescription="@string/upload_a_new_attachment"
android:visibility="gone"
app:backgroundTint="@color/defaultBrand"
app:srcCompat="@drawable/ic_file_upload_white_24dp"
tools:visibility="visible" />
+
+ <FrameLayout
+ android:id="@+id/pickerBackdrop"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@android:color/transparent"
+ android:visibility="gone" />
+
+ <LinearLayout
+ android:id="@+id/bottom_sheet_parent"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:orientation="vertical"
+ app:elevation="@dimen/spacer_1x"
+ app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/spacer_2x"
+ android:background="@drawable/bottom_sheet_rounded">
+
+ <View
+ android:layout_width="@dimen/spacer_4x"
+ android:layout_height="@dimen/spacer_1hx"
+ android:layout_alignParentBottom="true"
+ android:layout_centerInParent="true"
+ android:background="@color/bg_info_box" />
+ </RelativeLayout>
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/pickerRecyclerView"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?attr/colorPrimarySurface"
+ android:paddingStart="@dimen/spacer_1hx"
+ android:paddingTop="@dimen/spacer_1x"
+ android:paddingEnd="@dimen/spacer_1hx"
+ android:paddingBottom="@dimen/attachments_bottom_navigation_height"
+ tools:listitem="@layout/support_simple_spinner_dropdown_item"
+ tools:visibility="gone" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="bottom">
+
+ <com.google.android.material.bottomnavigation.BottomNavigationView
+ android:id="@+id/bottomNavigation"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/attachments_bottom_navigation_height"
+ android:translationY="@dimen/attachments_bottom_navigation_height"
+ app:backgroundTint="?attr/colorPrimary"
+ app:itemIconTint="?attr/colorAccent"
+ app:itemTextColor="?attr/colorAccent"
+ app:menu="@menu/attachment_picker_menu" />
+ </LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/item_attachment_default.xml b/app/src/main/res/layout/item_attachment_default.xml
index 98e3114eb..88e2126cd 100644
--- a/app/src/main/res/layout/item_attachment_default.xml
+++ b/app/src/main/res/layout/item_attachment_default.xml
@@ -9,23 +9,25 @@
android:padding="@dimen/spacer_2x">
<FrameLayout
- android:layout_width="wrap_content"
- android:layout_height="match_parent"
- android:layout_marginEnd="@dimen/spacer_1x">
+ android:layout_width="@dimen/avatar_size"
+ android:layout_height="@dimen/avatar_size"
+ android:layout_marginEnd="@dimen/spacer_2x">
- <androidx.appcompat.widget.AppCompatImageView
+ <ImageView
android:id="@+id/preview"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
android:layout_gravity="center"
- android:layout_margin="@dimen/spacer_1x"
+ android:contentDescription="@null"
+ android:padding="@dimen/spacer_1hx"
app:srcCompat="@drawable/ic_attach_file_grey600_24dp" />
- <androidx.appcompat.widget.AppCompatImageView
+ <ImageView
android:id="@+id/not_synced_yet"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
+ android:contentDescription="@string/not_synced_yet"
android:visibility="gone"
app:srcCompat="@drawable/ic_sync_blue_24dp"
tools:visibility="visible" />
@@ -36,6 +38,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
+ android:layout_marginEnd="@dimen/spacer_1x"
android:layout_weight="1"
android:textAppearance="?attr/textAppearanceListItem"
tools:maxLength="30"
diff --git a/app/src/main/res/layout/item_filter_user.xml b/app/src/main/res/layout/item_filter_user.xml
index 2c29bf5ad..0aced6dc2 100644
--- a/app/src/main/res/layout/item_filter_user.xml
+++ b/app/src/main/res/layout/item_filter_user.xml
@@ -31,7 +31,7 @@
</FrameLayout>
<TextView
- android:id="@+id/displayName"
+ android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
diff --git a/app/src/main/res/layout/item_photo_preview.xml b/app/src/main/res/layout/item_photo_preview.xml
new file mode 100644
index 000000000..280ed063c
--- /dev/null
+++ b/app/src/main/res/layout/item_photo_preview.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<it.niedermann.nextcloud.deck.ui.view.SquareConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/squareRelativeLayout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <androidx.camera.view.PreviewView
+ android:id="@+id/preview"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+ <ImageView
+ android:id="@+id/imageView"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:contentDescription="@string/take_photo"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHeight_percent=".3"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintWidth_percent=".3"
+ app:srcCompat="@drawable/ic_baseline_photo_camera_24"
+ app:tint="@android:color/white" />
+</it.niedermann.nextcloud.deck.ui.view.SquareConstraintLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/item_picker_native.xml b/app/src/main/res/layout/item_picker_native.xml
new file mode 100644
index 000000000..167cbd7ff
--- /dev/null
+++ b/app/src/main/res/layout/item_picker_native.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?attr/selectableItemBackground"
+ android:orientation="horizontal"
+ android:padding="@dimen/spacer_2x">
+
+ <ImageView
+ android:id="@+id/avatar"
+ android:layout_width="@dimen/avatar_size"
+ android:layout_height="@dimen/avatar_size"
+ android:layout_marginEnd="@dimen/spacer_2x"
+ android:contentDescription="@null"
+ android:padding="@dimen/spacer_1hx"
+ app:srcCompat="@drawable/ic_baseline_search_24" />
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:textAppearance="?attr/textAppearanceListItem"
+ tools:text="Search all files" />
+ </LinearLayout>
+
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="@color/bg_info_box" />
+
+ <TextView
+ android:id="@+id/subtitle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/spacer_2x"
+ android:paddingStart="72dp"
+ android:paddingTop="@dimen/spacer_1x"
+ android:paddingEnd="@dimen/spacer_1x"
+ android:paddingBottom="@dimen/spacer_1x"
+ android:text="@string/recent"
+ android:textAppearance="?attr/textAppearanceOverline" />
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/item_picker_user.xml b/app/src/main/res/layout/item_picker_user.xml
new file mode 100644
index 000000000..f9a17508e
--- /dev/null
+++ b/app/src/main/res/layout/item_picker_user.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?attr/selectableItemBackground"
+ android:gravity="center_vertical"
+ android:orientation="horizontal"
+ android:padding="@dimen/spacer_2x">
+
+ <FrameLayout
+ android:layout_width="@dimen/avatar_size"
+ android:layout_height="@dimen/avatar_size"
+ android:layout_marginEnd="@dimen/spacer_2x">
+
+ <ImageView
+ android:id="@+id/avatar"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:contentDescription="@null"
+ tools:background="@color/board_default_color" />
+
+ <TextView
+ android:id="@+id/initials"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center"
+ android:textColor="@android:color/white"
+ android:textSize="20dp"
+ android:translationY="-2dp"
+ android:visibility="gone"
+ tools:ignore="SpUsage"
+ tools:text="G"
+ tools:visibility="visible" />
+
+ </FrameLayout>
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:textAppearance="?attr/textAppearanceListItem"
+ tools:text="@tools:sample/full_names" />
+
+ <TextView
+ android:id="@+id/contactInformation"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:textAppearance="?attr/textAppearanceListItemSecondary"
+ tools:text="@tools:sample/us_phones" />
+ </LinearLayout>
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/menu/attachment_picker_menu.xml b/app/src/main/res/menu/attachment_picker_menu.xml
new file mode 100644
index 000000000..f280ac752
--- /dev/null
+++ b/app/src/main/res/menu/attachment_picker_menu.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+ <item
+ android:id="@+id/gallery"
+ android:icon="@drawable/ic_baseline_photo_camera_24"
+ android:title="@string/gallery"
+ app:showAsAction="ifRoom" />
+ <item
+ android:id="@+id/contacts"
+ android:icon="@drawable/ic_person_grey600_24dp"
+ android:title="@string/contacts"
+ app:showAsAction="ifRoom" />
+ <item
+ android:id="@+id/files"
+ android:icon="@drawable/ic_attach_file_grey600_24dp"
+ android:title="@string/files"
+ app:showAsAction="ifRoom" />
+</menu>
diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml
index abb3667fe..cf90f44c6 100644
--- a/app/src/main/res/values-ca/strings.xml
+++ b/app/src/main/res/values-ca/strings.xml
@@ -282,4 +282,4 @@
<string name="simple_move">Mou</string>
<string name="cannot_upload_files_without_permission">No es poden pujar fitxers sense permís</string>
<string name="simple_unassign">No ho assignis</string>
-</resources>
+ </resources>
diff --git a/app/src/main/res/values-cs-rCZ/strings.xml b/app/src/main/res/values-cs-rCZ/strings.xml
index 33354922d..170d1a877 100644
--- a/app/src/main/res/values-cs-rCZ/strings.xml
+++ b/app/src/main/res/values-cs-rCZ/strings.xml
@@ -294,4 +294,11 @@
<string name="simple_clone">Klonovat</string>
<string name="user_avatar">Zástupný obrázek uživatele</string>
<string name="simple_unassign">Zrušit přirazení</string>
-</resources>
+ <string name="simple_contact">Kontakt</string>
+ <string name="simple_file">Soubor</string>
+ <string name="simple_camera">Kamera</string>
+ <string name="min_api_21">Tato funkce vyžaduje minimálně verzi 5 systému Android</string>
+ <string name="take_photo">Vyfotit</string>
+ <string name="take_photo_switch_camera">Přepnout kameru</string>
+ <string name="take_photo_toggle_torch">Vyp/zap. přisvícení</string>
+ </resources>
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 7d6ef9abe..832518a18 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -58,7 +58,7 @@
<string name="about_source_title">Quellcode</string>
<string name="about_source">Dieses Projekt wird auf GitHub gehostet: %1$s</string>
<string name="about_issues_title">Probleme</string>
- <string name="about_issues">Sie können Fehler, Verbesserungsvorschläge oder Wünsche für neue Funktionen im GitHub-Ticket-System erfassen: %1$s</string>
+ <string name="about_issues">Sie können Fehler, Verbesserungsvorschläge oder Wünsche für neue Funktionen als Issue auf GitHub erfassen: %1$s</string>
<string name="about_translate_title">Übersetzen</string>
<string name="about_translate">Treten Sie dem Nextcloud-Team bei Transifex bei und helfen Sie uns, diese App zu übersetzen: %1$s</string>
<string name="about_app_license_title">App-Lizenz</string>
@@ -101,10 +101,10 @@
<string name="card_edit_attachments">Anhänge</string>
<string name="card_edit_activity">Aktivitäten</string>
<string name="about_server_app_version_text">Serverversion der App:</string>
- <string name="no_files_attached_to_this_card">An diese Karte sind keine Dateien angehängt.</string>
+ <string name="no_files_attached_to_this_card">Dieser Karte sind keine Dateien angehängt.</string>
<string name="attachments">Anhänge</string>
<string name="no_cards">Bislang keine Karten</string>
- <string name="no_account">Kein Konto konfiguriert</string>
+ <string name="no_account">Kein Konto eingerichtet</string>
<string name="account_already_added">Konto wurde bereits hinzugefügt</string>
<string name="account_is_getting_imported">Konto wird importiert</string>
<string name="not_synced_yet">Noch nicht synchronisiert</string>
@@ -286,4 +286,20 @@
<string name="simple_clone">Klonen</string>
<string name="user_avatar">Benutzer-Avatar</string>
<string name="simple_unassign">Zuordnung aufheben</string>
+ <string name="simple_contact">Kontakt</string>
+ <string name="simple_file">Datei</string>
+ <string name="simple_camera">Kamera</string>
+ <string name="min_api_21">Diese Funktion benötigt Android 5 oder neuer</string>
+ <string name="take_photo">Foto aufnehmen</string>
+ <string name="take_photo_switch_camera">Kamera wechseln</string>
+ <string name="take_photo_toggle_torch">Taschenlampe umschalten</string>
+ <string name="show_all_contacts">Alle Kontakte anzeigen</string>
+ <string name="show_all_files">Alle Dateien anzeigen</string>
+ <string name="recent">Neueste</string>
+ <string name="upload_a_new_attachment">Neuen Anhang hochladen</string>
+ <string name="contacts">Kontakte</string>
+ <string name="downloads">Downloads</string>
+ <string name="files">Dateien</string>
+ <string name="gallery">Galerie</string>
+ <string name="simple_attach">anhängen</string>
</resources>
diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml
index 5766f15f2..877578f2d 100644
--- a/app/src/main/res/values-el/strings.xml
+++ b/app/src/main/res/values-el/strings.xml
@@ -282,4 +282,4 @@
<string name="simple_move">Μετακίνηση</string>
<string name="cannot_upload_files_without_permission">Δεν μπορείτε να ανεβάσετε αρχεία χωρίς άδεια</string>
<string name="simple_unassign">Αποδέσμευση</string>
-</resources>
+ </resources>
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 0287028ae..a54158ba9 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -215,7 +215,7 @@
<string name="error_dialog_title">Oh no - ¿Ahora qué? 🙁</string>
<string name="error_dialog_tip_token_mismatch_retry">Por favor, intenta forzar el cierre de la aplicación y vuelve a abrirla. Es posible que haya habido un problema de conexión con la aplicación Nextcloud.</string>
- <string name="error_dialog_tip_clear_storage_might_help">Si el problema persiste, intenta limpiar borrar el almacenamiento de ambas apps, Nextcloud y Nextcloud Deck para solucionar este problema.</string>
+ <string name="error_dialog_tip_clear_storage_might_help">Si el problema persiste, intenta borrar el almacenamiento de ambas apps: Nextcloud y Nextcloud Deck para solucionar este problema.</string>
<string name="error_dialog_tip_database_upgrade_failed">Falló la actualización de la base de datos. Por favor, informe del problema y limpie el almacenamiento para usar la aplicación de manera normal.</string>
<string name="error_dialog_tip_clear_storage">Puedes limpiar el almacenamiento abriendo la información de la app y seleccionando Almacenamiento → Eliminar datos.</string>
<string name="error_dialog_tip_files_outdated">Su aplicación Nextcloud parece que está desactualizada. Por favor visite la Play Store o F-Droid para conseguir la versión más reciente.</string>
@@ -230,7 +230,7 @@
<string name="error_dialog_we_need_info">Necesitamos la siguiente información técnica para ayudarle:</string>
<string name="error_dialog_redirect">Tu servidor respondió con el código de estado HTTP 302, lo que implica que no está instalada la aplicación Deck en su servidor o que algo está mal configurado. Esto puede estar causado por anulaciones personalizadas en un archivo .htaccess o por aplicaciones de Nexcloud como OID Client. </string>
<string name="error_dialog_version_not_parsable">No se pudo determinar la versión de la aplicación Deck en el lado del servidor. Por favor, compruebe que está instalada y activada. </string>
- <string name="error_dialog_account_might_not_be_authorized">La cuenta de tu app Nextcloud puede ya no estar autorizada.</string>
+ <string name="error_dialog_account_might_not_be_authorized">La cuenta de tu app Nextcloud podría no estar autorizada.</string>
<string name="error_dialog_user_not_found_in_database">El usuario actual no coincide con el usuario que tenemos en nuestra base de datos. Si estás usando LDAP en tu instancia de Nextcloud, tu app de Nextcloud puede haber almacenado una ID de usuario antigua.</string>
<string name="error_dialog_capabilities_not_parsable">No se ha podido comprobar las capacidades de su servidor. Por favor compruebe que su servidor está funcionando bien y que otras aplicaciones de cliente son capaces de acceder a Nextcloud.</string>
<string name="error_dialog_attachment_upload_failed">No se ha podido subir un adjunto. Por favor, intenta compartirlo de otra forma y comunícanos este fallo.</string>
@@ -286,4 +286,20 @@
<string name="simple_clone">Clonar</string>
<string name="user_avatar">Usar avatar</string>
<string name="simple_unassign">Sin asignar</string>
+ <string name="simple_contact">Contacto</string>
+ <string name="simple_file">Archivo</string>
+ <string name="simple_camera">Camera</string>
+ <string name="min_api_21">Esta característica requiere Android 5 como mínimo</string>
+ <string name="take_photo">Tomar una foto</string>
+ <string name="take_photo_switch_camera">Cambiar cámara</string>
+ <string name="take_photo_toggle_torch">Activar/desactivar linterna</string>
+ <string name="show_all_contacts">Mostrar todos los contactos</string>
+ <string name="show_all_files">Mostrar todos los archivos</string>
+ <string name="recent">Reciente</string>
+ <string name="upload_a_new_attachment">Subir un nuevo adjunto</string>
+ <string name="contacts">Contactos</string>
+ <string name="downloads">Descargas</string>
+ <string name="files">Archivos</string>
+ <string name="gallery">Galería</string>
+ <string name="simple_attach">adjuntar</string>
</resources>
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index 9edbf2e36..c1820e9e8 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -111,6 +111,7 @@
<string name="no_lists_yet">Aucune liste pour l\'instant</string>
<string name="do_you_want_to_save_your_changes">Souhaitez-vous enregistrer vos modifications ?</string>
<string name="do_you_want_to_archive_all_cards_of_the_list">Souhaitez-vous archiver toutes les carte de %1$s ?</string>
+ <string name="do_you_want_to_archive_all_cards_of_the_filtered_list">Souhaitez-vous archiver toutes les cartes de %1$s affichées par le filtre?</string>
<plurals name="do_you_want_to_delete_the_current_list">
<item quantity="one">Cela supprimera définitivement l\'ensemble des %1$d cartes de cette liste.</item>
<item quantity="other">Cela supprimera définitivement l\'ensemble des %1$d cartes de cette liste.</item>
@@ -281,4 +282,20 @@
<string name="project_type_room">Salon de discussion</string>
<string name="simple_move">Déplacer</string>
<string name="cannot_upload_files_without_permission">Impossible de téléverser des fichiers sans permission</string>
+ <string name="clone_cards">Cloner les cartes</string>
+ <string name="simple_clone">Cloner</string>
+ <string name="user_avatar">Avatar utilisateur</string>
+ <string name="simple_unassign">Désassigner</string>
+ <string name="simple_contact">Contact</string>
+ <string name="simple_file">Fichier</string>
+ <string name="simple_camera">Appareil photo</string>
+ <string name="min_api_21">Cette fonctionnalité est disponible à partir d\'Android 5</string>
+ <string name="take_photo">Prendre une photo</string>
+ <string name="take_photo_switch_camera">Changer d\'appareil photo</string>
+ <string name="take_photo_toggle_torch">Allumer la lampe</string>
+ <string name="show_all_files">Afficher tous les fichiers</string>
+ <string name="contacts">Contacts</string>
+ <string name="downloads">Téléchargements</string>
+ <string name="files">Fichiers</string>
+ <string name="gallery">Galerie</string>
</resources>
diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml
index d2e9ae886..515172758 100644
--- a/app/src/main/res/values-gl/strings.xml
+++ b/app/src/main/res/values-gl/strings.xml
@@ -286,4 +286,20 @@
<string name="simple_clone">Clonar</string>
<string name="user_avatar">Avatar do usuario</string>
<string name="simple_unassign">Desasignar</string>
+ <string name="simple_contact">Contacto</string>
+ <string name="simple_file">Ficheiro</string>
+ <string name="simple_camera">Cámara</string>
+ <string name="min_api_21">Esta función require polo menos Android 5</string>
+ <string name="take_photo">Tirar unha foto</string>
+ <string name="take_photo_switch_camera">Cambiar de cámara</string>
+ <string name="take_photo_toggle_torch">Alternar o facho</string>
+ <string name="show_all_contacts">Amosar todos os contactos</string>
+ <string name="show_all_files">Amosar todos os ficheiros</string>
+ <string name="recent">Recente</string>
+ <string name="upload_a_new_attachment">Enviar un novo anexo</string>
+ <string name="contacts">Contactos</string>
+ <string name="downloads">Descargas</string>
+ <string name="files">Ficheiros</string>
+ <string name="gallery">Galería</string>
+ <string name="simple_attach">anexar</string>
</resources>
diff --git a/app/src/main/res/values-hu-rHU/strings.xml b/app/src/main/res/values-hu-rHU/strings.xml
index 1b18a371d..29007d971 100644
--- a/app/src/main/res/values-hu-rHU/strings.xml
+++ b/app/src/main/res/values-hu-rHU/strings.xml
@@ -286,4 +286,4 @@
<string name="simple_clone">Klónozás</string>
<string name="user_avatar">Felhasználói avatar</string>
<string name="simple_unassign">Elvétel</string>
-</resources>
+ </resources>
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index f959b76fe..ac9f496f1 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -286,4 +286,20 @@
<string name="simple_clone">Clona</string>
<string name="user_avatar">Avatar dell\'utente</string>
<string name="simple_unassign">Rimuovi assegnazione</string>
+ <string name="simple_contact">Contatto</string>
+ <string name="simple_file">File</string>
+ <string name="simple_camera">Fotocamera</string>
+ <string name="min_api_21">Questa funzionalità richiede almeno Android 5</string>
+ <string name="take_photo">Scatta una foto</string>
+ <string name="take_photo_switch_camera">Cambia fotocamera</string>
+ <string name="take_photo_toggle_torch">Attiva la torcia</string>
+ <string name="show_all_contacts">Mostra tutti i contatti</string>
+ <string name="show_all_files">Mostra tutti i file</string>
+ <string name="recent">Recenti</string>
+ <string name="upload_a_new_attachment">Carica un nuovo allegato</string>
+ <string name="contacts">Contatti</string>
+ <string name="downloads">Scaricamenti</string>
+ <string name="files">File</string>
+ <string name="gallery">Galleria</string>
+ <string name="simple_attach">allega</string>
</resources>
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index 057d6e8d3..67af469be 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -294,4 +294,20 @@
<string name="simple_clone">Klonuj</string>
<string name="user_avatar">Awatar użytkownika</string>
<string name="simple_unassign">Cofnij przypisanie</string>
+ <string name="simple_contact">Kontakt</string>
+ <string name="simple_file">Plik</string>
+ <string name="simple_camera">Aparat</string>
+ <string name="min_api_21">Funkcja ta wymaga co najmniej Androida 5</string>
+ <string name="take_photo">Zrobić zdjęcie</string>
+ <string name="take_photo_switch_camera">Przełącz aparat</string>
+ <string name="take_photo_toggle_torch">Włącz latarkę</string>
+ <string name="show_all_contacts">Pokaż wszystkie kontakty</string>
+ <string name="show_all_files">Pokaż wszystkie pliki</string>
+ <string name="recent">Ostatnie</string>
+ <string name="upload_a_new_attachment">Prześlij nowy załącznik</string>
+ <string name="contacts">Kontakty</string>
+ <string name="downloads">Pobrane</string>
+ <string name="files">Pliki</string>
+ <string name="gallery">Galeria</string>
+ <string name="simple_attach">dołącz</string>
</resources>
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index 9cf3f4f6d..c88975abb 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -286,4 +286,11 @@
<string name="simple_clone">Clonar</string>
<string name="user_avatar">Avatar do usuário</string>
<string name="simple_unassign">Desatribuir</string>
-</resources>
+ <string name="simple_contact">Contato</string>
+ <string name="simple_file">Arquivo</string>
+ <string name="simple_camera">Câmera</string>
+ <string name="min_api_21">Este recurso requer pelo menos Android 5</string>
+ <string name="take_photo">Tire uma foto</string>
+ <string name="take_photo_switch_camera">Trocar câmera</string>
+ <string name="take_photo_toggle_torch">Alternar tocha</string>
+ </resources>
diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml
index 0e3aa0be8..b7328a127 100644
--- a/app/src/main/res/values-sl/strings.xml
+++ b/app/src/main/res/values-sl/strings.xml
@@ -294,4 +294,11 @@
<string name="simple_clone">Kloniraj</string>
<string name="user_avatar">Podoba uporabnika</string>
<string name="simple_unassign">Odstrani</string>
-</resources>
+ <string name="simple_contact">Stik</string>
+ <string name="simple_file">Datoteka</string>
+ <string name="simple_camera">Kamera</string>
+ <string name="min_api_21">Ta možnost zahteva vsaj sistem Android 5.</string>
+ <string name="take_photo">Zajemi posnetek</string>
+ <string name="take_photo_switch_camera">Preklopi kamero</string>
+ <string name="take_photo_toggle_torch">Preklopi bliskavico</string>
+ </resources>
diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml
index 223e72c32..ec1cfa296 100644
--- a/app/src/main/res/values-sr/strings.xml
+++ b/app/src/main/res/values-sr/strings.xml
@@ -290,4 +290,4 @@
<string name="simple_clone">Клонирај</string>
<string name="user_avatar">Кориснички аватар</string>
<string name="simple_unassign">Уклони доделу</string>
-</resources>
+ </resources>
diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml
index 82d5a3636..98bd27e93 100644
--- a/app/src/main/res/values-sv/strings.xml
+++ b/app/src/main/res/values-sv/strings.xml
@@ -27,7 +27,7 @@
<string name="simple_clear">Rensa</string>
<string name="simple_discard">kassera</string>
<string name="simple_update">Uppdatera</string>
- <string name="simple_delete">Radera</string>
+ <string name="simple_delete">Ta bort</string>
<string name="simple_rename">Byt namn</string>
<string name="simple_settings">Inställningar</string>
<string name="simple_undo">Ångra</string>
@@ -42,8 +42,8 @@
<string name="edit_board">Ändra tavla</string>
<string name="archive_board">Arkivera tavla</string>
- <string name="delete_board">Radera tavla</string>
- <string name="delete_something">Radera %1$s</string>
+ <string name="delete_board">Ta bort tavla</string>
+ <string name="delete_something">Ta bort %1$s</string>
<!-- About -->
<string name="about">Om</string>
@@ -86,12 +86,12 @@
<string name="activity">Aktivitet</string>
<string name="add_list">Lägg till lista...</string>
<string name="rename_list">Byt namn på lista</string>
- <string name="delete_list">Radera lista</string>
+ <string name="delete_list">Ta bort lista</string>
<string name="label_menu">meny</string>
<string name="action_card_assign">Tilldela kort till mig</string>
<string name="action_card_unassign">Ta bort kort från mig</string>
<string name="action_card_archive">Arkivera kort</string>
- <string name="action_card_delete">Radera kort</string>
+ <string name="action_card_delete">Ta bort kort</string>
<string name="add_board">Lägg till tavla</string>
<string name="label_clear_due_date">Rensa slutdatum</string>
@@ -111,6 +111,7 @@
<string name="no_lists_yet">Inga listor än</string>
<string name="do_you_want_to_save_your_changes">Vill du spara dina ändringar?</string>
<string name="do_you_want_to_archive_all_cards_of_the_list">Vill du arkivera alla kort av %1$s?</string>
+ <string name="do_you_want_to_archive_all_cards_of_the_filtered_list">Vill du arkivera alla filtrerade kort av %1$s?</string>
<plurals name="do_you_want_to_delete_the_current_list">
<item quantity="one">Detta kommer att permanent radera %1$d kortet i denna lista.</item>
<item quantity="other">Detta kommer att permanent radera alla %1$d kort i denna lista.</item>
@@ -225,11 +226,15 @@
<string name="error_dialog_check_server">Inkorrekt svar från din server. Kontrollera att du har tillgång till Deck-appen genom din webbläsare.</string>
<string name="error_dialog_check_server_logs">Det finns problem med din Nextcloud-installation. Ta gärna en titt i serverns loggfiler. </string>
<string name="error_dialog_check_maintenance">Kontrollera att underhållsläge inte är aktivt på Nextcloud-installation.</string>
- <string name="error_dialog_insufficient_storage">Din Nextcloud-installation har slut på diskutrymme. Radera några filer för att kunna synkronisera ändringar till ditt moln.</string>
+ <string name="error_dialog_insufficient_storage">Din Nextcloud-installation har slut på diskutrymme. Ta bort några filer för att kunna synkronisera ändringar till ditt moln.</string>
<string name="error_dialog_we_need_info">Vi behöver följande teknisk information för att kunna hjälpa dig:</string>
<string name="error_dialog_redirect">Din server svarade med ett HTTP 302 statusmeddelande vilket innebär att du inte har installerat Deck appen på din server eller att något är felkonfigurerat. Detta kan orsakas av manuella ändringar i en .htaccess-fil eller av Nextcloud-appar som \"OID Client\".</string>
<string name="error_dialog_version_not_parsable">Vi kunde inte avgöra vilken version av Deck-appen som är installerad på servern. Kontrollera att den är installerad och aktiverad.</string>
<string name="error_dialog_account_might_not_be_authorized">Ditt Nextcloud-konto är eventuellt inte längre auktoriserat. </string>
+ <string name="error_dialog_user_not_found_in_database">Den nuvarande användaren matchar inte användaren vi har i vår databas. Om ni använder LDAP i er Nextcloud-instans kan din Nextcloud-app ha lagrat ett gammalt användar-ID.</string>
+ <string name="error_dialog_capabilities_not_parsable">Vi kunde inte hämta er servers förmågor. Vänligen säkerställ att er server fungerar som den ska och att klient-appar kan komma åt Nextcloud.</string>
+ <string name="error_dialog_attachment_upload_failed">En bifogad fil kunde inte laddas upp. Försök dela den på ett annat och sätt och vänligen berätta för oss om den här buggen.</string>
+ <string name="error_dialog_tip_disable_battery_optimizations">Stäng av alla batterioptimeringar för Nextcloud och Deck-appen</string>
<string name="error_action_open_deck_info">Öppna appinformation</string>
<string name="error_action_open_network">Nätverksinställningar</string>
<string name="error_action_server_logs">Serverloggar</string>
@@ -247,11 +252,38 @@
<string name="append_text_to_description">Lägg till beskrivning</string>
<string name="add_text_as_comment">Lägg till som kommentar</string>
<string name="progress_count">%1$d av %2$d</string>
+ <plurals name="progress_error_count">
+ <item quantity="one">%1$d fel vid uppladdning</item>
+ <item quantity="other">%1$d fel vid uppladdning</item>
+ </plurals>
<string name="simple_report">Rapportera</string>
+ <string name="error_action_open_battery_settings">Batteriinställningar</string>
+ <string name="move_warning">Varken kommentarer eller bifogade filer följer med när man flyttar kortet till ett annat plank.</string>
<string name="clone_board">Kopiera tavla</string>
+ <string name="cloning_board">Klonar %1$s ...</string>
+ <string name="successfully_cloned_board">Lyckades klona %1$s</string>
+ <string name="attachment_does_not_yet_exist">Bifogade filen finns inte ännu i Deck</string>
+ <string name="card_does_not_yet_exist">Kortet finns inte ännu i Deck</string>
+
<string name="widget_stack_title">Lista</string>
+ <string name="widget_stack_header_icon">Ikon för widgetens sidhuvud</string>
+ <string name="widget_stack_placeholder_icon">Ikon för widgetens platshållare</string>
+ <string name="select_stack">Välj lista</string>
+ <string name="project_type_deck_board">Deck-plank</string>
+ <string name="project_type_deck_card">Deck-kort</string>
<string name="project_type_file">Fil</string>
<string name="projects_title">Projekt</string>
+ <plurals name="resources_count">
+ <item quantity="one">%1$d resurs</item>
+ <item quantity="other">%1$d resurser</item>
+ </plurals>
+ <string name="no_assigned_label">Ingen tilldelad tagg</string>
+ <string name="single_card">Enskilt kort</string>
+ <string name="project_type_room">Talk-rum</string>
<string name="simple_move">Flytta</string>
+ <string name="cannot_upload_files_without_permission">Kan inte ladda upp filer utan tillåtelse</string>
+ <string name="clone_cards">Klona kort</string>
+ <string name="simple_clone">Klona</string>
+ <string name="user_avatar">Användar-avatar</string>
<string name="simple_unassign">Ta bort tilldelning</string>
-</resources>
+ </resources>
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index 194520815..60ff93521 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -286,4 +286,20 @@
<string name="simple_clone">Kopyala</string>
<string name="user_avatar">Kullanıcı avatarı</string>
<string name="simple_unassign">Atamayı kaldır</string>
+ <string name="simple_contact">Kişi</string>
+ <string name="simple_file">Dosya</string>
+ <string name="simple_camera">Kamera</string>
+ <string name="min_api_21">Bu özellik için en az Android 5 sürümü gereklidir</string>
+ <string name="take_photo">Bir fotoğraf çekin</string>
+ <string name="take_photo_switch_camera">Kamerayı değiştir</string>
+ <string name="take_photo_toggle_torch">Feneri aç/kapat</string>
+ <string name="show_all_contacts">Tüm kişileri görüntüle</string>
+ <string name="show_all_files">Tüm dosyaları görüntüle</string>
+ <string name="recent">Son kullanılan</string>
+ <string name="upload_a_new_attachment">Ek dosya yükle</string>
+ <string name="contacts">Kişiler</string>
+ <string name="downloads">İndirmeler</string>
+ <string name="files">Dosyalar</string>
+ <string name="gallery">Galeri</string>
+ <string name="simple_attach">ekle</string>
</resources>
diff --git a/app/src/main/res/values-v21/styles.xml b/app/src/main/res/values-v21/styles.xml
new file mode 100644
index 000000000..61e9c4157
--- /dev/null
+++ b/app/src/main/res/values-v21/styles.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <style name="AppBottomSheetDialogTheme" parent="Theme.MaterialComponents.DayNight.BottomSheetDialog">
+ <item name="bottomSheetStyle">@style/AppModalStyle</item>
+ <item name="android:windowIsFloating">false</item>
+ <item name="android:navigationBarColor">@color/primary</item>
+ <item name="android:statusBarColor">@android:color/transparent</item>
+ </style>
+</resources> \ No newline at end of file
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index 428e77ed4..9aea4a1ba 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -211,21 +211,29 @@
<string name="error_dialog_tip_files_force_stop">您的 Nextcloud 应用似乎出现了问题。请尝试强行停止 Nextcloud 和 Nextcloud 看板两个应用。</string>
<string name="error_dialog_tip_files_delete_storage">如果强行停止没有效果,您可以尝试清除两个应用的存储空间。</string>
<string name="error_dialog_timeout_instance">服务器没有在规定的时间内回答。请确保您的服务器实例在正常运行。</string>
+ <string name="error_dialog_timeout_toggle">检查您的网络连接。有时候关闭并重新开启移动数据或 Wi-Fi 有帮助。</string>
<string name="error_dialog_check_server">服务器的回答不正确。请检查您是否能通过网页访问看板应用。</string>
<string name="error_dialog_check_server_logs">您的 Nextcloud 配置有一些问题。请查看服务器日志。</string>
<string name="error_dialog_check_maintenance">请检查您的 Nextcloud 实例是否处于维护模式。</string>
<string name="error_dialog_insufficient_storage">您的 Nextcloud 实例已经没有可用的存储空间了。请删除一些文件来允许本地的变更同步到云。</string>
<string name="error_dialog_we_need_info">我们需要以下技术信息来帮助您:</string>
+ <string name="error_dialog_account_might_not_be_authorized">您的Nextcloud应用账号可能不再被授权。</string>
+ <string name="error_action_open_network">网络设置</string>
+ <string name="error_action_server_logs">服务器日志</string>
+ <string name="error_action_install">安装</string>
<string name="error_action_report_issue">报告</string>
<string name="info_box_maintenance_mode">服务器处于维护模式</string>
<string name="share_link">共享链接</string>
<string name="manage_accounts">管理账号</string>
<string name="simple_reply">回复</string>
<string name="simple_report">报告</string>
+ <string name="error_action_open_battery_settings">电池设置</string>
<string name="clone_board">克隆面板</string>
<string name="widget_stack_title">列表</string>
<string name="project_type_file">文件</string>
<string name="projects_title">项目</string>
<string name="simple_move">移动</string>
+ <string name="simple_clone">克隆</string>
+ <string name="user_avatar">用户头像</string>
<string name="simple_unassign">取消分配</string>
-</resources>
+ </resources>
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index 8aa82c1cf..c19fe8de1 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -7,6 +7,7 @@
<dimen name="spacer_4x">32dp</dimen>
<dimen name="compact_label_height">6dp</dimen>
+ <dimen name="attachments_bottom_navigation_height">64dp</dimen>
<!-- Drawer header -->
<dimen name="drawer_header_height">100dp</dimen>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 32f5aa1e2..d6d9470fe 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -311,4 +311,20 @@
<string name="simple_clone">Clone</string>
<string name="user_avatar">User avatar</string>
<string name="simple_unassign">Unassign</string>
+ <string name="simple_contact">Contact</string>
+ <string name="simple_file">File</string>
+ <string name="simple_camera">Camera</string>
+ <string name="min_api_21">This feature requires at least Android 5</string>
+ <string name="take_photo">Take a photo</string>
+ <string name="take_photo_switch_camera">Switch camera</string>
+ <string name="take_photo_toggle_torch">Toggle torch</string>
+ <string name="show_all_contacts">Show all contacts</string>
+ <string name="show_all_files">Show all files</string>
+ <string name="recent">Recent</string>
+ <string name="upload_a_new_attachment">Upload a new attachment</string>
+ <string name="contacts">Contacts</string>
+ <string name="downloads">Downloads</string>
+ <string name="files">Files</string>
+ <string name="gallery">Gallery</string>
+ <string name="simple_attach">attach</string>
</resources>
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index c253ae461..9881f8e76 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -7,6 +7,15 @@
<item name="toolbarStyle">@style/toolbarStyle</item>
<item name="android:windowBackground">?attr/colorPrimary</item>
<item name="textAppearanceHeadline1">@style/Deck.TextAppearance.Headline1</item>
+ <item name="bottomSheetDialogTheme">@style/AppBottomSheetDialogTheme</item>
+ </style>
+
+ <style name="AppBottomSheetDialogTheme" parent="Theme.MaterialComponents.DayNight.BottomSheetDialog">
+ <item name="bottomSheetStyle">@style/AppModalStyle</item>
+ </style>
+
+ <style name="AppModalStyle" parent="Widget.Design.BottomSheet.Modal">
+ <item name="android:background">@drawable/bottom_sheet_rounded</item>
</style>
<style name="toolbarStyle" parent="@style/Widget.AppCompat.Toolbar">
@@ -34,6 +43,11 @@
<item name="android:windowIsTranslucent">true</item>
</style>
+ <style name="TakePhotoTheme" parent="TransparentTheme">
+ <item name="android:windowFullscreen">true</item>
+ <item name="android:windowContentOverlay">@null</item>
+ </style>
+
<style name="Deck.TextAppearance.Headline1" parent="TextAppearance.MaterialComponents.Headline1">
<item name="android:textSize">36sp</item>
</style>