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
diff options
context:
space:
mode:
authorStefan Niedermann <info@niedermann.it>2020-12-09 19:59:07 +0300
committerStefan Niedermann <info@niedermann.it>2020-12-09 19:59:07 +0300
commitdf900e53492c7b30cbb90b9180d6c3cdf59f38d9 (patch)
treeb88dc0386e6b448c95cf4fcb46fbeaf8edc8780f /app/src/main/java/it/niedermann/nextcloud/deck/ui
parent034ae108ae4ab4c273ef4d74f1bfd39fbc4d8a84 (diff)
parentf29eed9db4c0906fa7887e446cf0325718ef6827 (diff)
Merge branch 'master' into fastlanefastlane
# Conflicts: # fastlane/metadata/android/en-US/images/phoneScreenshots/1.png # fastlane/metadata/android/en-US/images/phoneScreenshots/2.png # fastlane/metadata/android/en-US/images/phoneScreenshots/4.png
Diffstat (limited to 'app/src/main/java/it/niedermann/nextcloud/deck/ui')
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/ImportAccountActivity.java8
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/MainActivity.java315
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/MainViewModel.java223
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/PickStackActivity.java115
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/PushNotificationActivity.java146
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/PushNotificationViewModel.java52
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/about/AboutFragmentLicenseTab.java6
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherDialog.java26
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherViewHolder.java7
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardViewHolder.java37
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardsActvitiy.java43
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedcards/ArchivedCardsActvitiy.java22
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedcards/ArchivedCardsAdapter.java72
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentAdapter.java56
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentViewHolder.java61
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentsActivity.java20
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentsViewModel.java25
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/board/ArchiveBoardListener.java1
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/board/BoardAdapter.java2
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/board/EditBoardDialogFragment.java12
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/board/EditBoardListener.java4
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/board/accesscontrol/AccessControlAdapter.java7
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/board/accesscontrol/AccessControlDialogFragment.java24
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/EditLabelDialogFragment.java4
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/ManageLabelsDialogFragment.java16
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/ManageLabelsViewHolder.java8
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedAlertDialogBuilder.java4
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedDatePickerDialog.java8
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedDeleteAlertDialogBuilder.java3
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedSnackbar.java6
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedTimePickerDialog.java12
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandingUtil.java17
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/AbstractCardViewHolder.java122
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CardAdapter.java345
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CardOptionsItemSelectedListener.java11
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CompactCardViewHolder.java84
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/DefaultCardOnlyTitleViewHolder.java61
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/DefaultCardViewHolder.java157
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditActivity.java44
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditCardViewModel.java78
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/ItemCardViewHolder.java15
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/LabelAutoCompleteAdapter.java9
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/UserAutoCompleteAdapter.java17
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/activities/CardActivityFragment.java9
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/activities/CardActivityViewHolder.java50
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/assignee/CardAssigneeDialog.java113
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/assignee/CardAssigneeListener.java11
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/AttachmentDeletedListener.java4
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/AttachmentViewHolder.java43
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentAdapter.java125
-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.java492
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/DefaultAttachmentViewHolder.java36
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/ImageAttachmentViewHolder.java33
-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/card/comments/CardCommentsFragment.java23
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsMentionProposer.java139
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CommentsViewModel.java37
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/ItemCommentViewHolder.java19
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/util/CommentsUtil.java48
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/AssigneeAdapter.java80
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/AssigneeDecoration.java28
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/AssigneeViewHolder.java29
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardDetailsFragment.java253
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardDetailsListener.java21
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardProjectsAdapter.java52
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardProjectsViewHolder.java32
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/projectresources/CardProjectResourceAdapter.java54
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/projectresources/CardProjectResourceViewHolder.java110
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/card/projectresources/CardProjectResourcesDialog.java83
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionActivity.java29
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionDialogFragment.java10
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionHandler.java19
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/tips/TipsAdapter.java23
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterDialogFragment.java18
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterLabelsAdapter.java44
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterLabelsFragment.java25
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserAdapter.java43
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserFragment.java29
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterViewModel.java40
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/SelectionListener.java6
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountViewHolder.java6
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountsActivity.java28
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountsViewModel.java45
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/movecard/MoveCardDialogFragment.java128
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/movecard/MoveCardListener.java5
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackFragment.java204
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackListener.java12
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackViewModel.java45
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/AccountAdapter.java27
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/BoardAdapter.java8
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/PrepareCreateActivity.java226
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/StackAdapter.java16
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/settings/SettingsFragment.java4
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/sharetarget/ShareProgressDialogFragment.java5
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/sharetarget/ShareTargetActivity.java10
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/StackAdapter.java12
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/StackFragment.java49
-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/ColorChooser.java56
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/view/OverlappingAvatars.java16
-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/ui/view/labelchip/CompactLabelChip.java21
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labelchip/DefaultLabelChip.java21
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labelchip/LabelChip.java (renamed from app/src/main/java/it/niedermann/nextcloud/deck/ui/view/LabelChip.java)25
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labellayout/CompactLabelLayout.java22
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labellayout/DefaultLabelLayout.java21
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labellayout/LabelLayout.java (renamed from app/src/main/java/it/niedermann/nextcloud/deck/ui/view/LabelLayout.java)19
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/singlecard/SelectCardForWidgetActivity.java2
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/singlecard/SingleCardWidget.java7
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidget.java121
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetConfigurationActivity.java68
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetConfigurationViewModel.java23
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetFactory.java134
-rw-r--r--app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetService.java11
130 files changed, 5680 insertions, 1531 deletions
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 854b99a6a..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
@@ -23,6 +23,7 @@ import com.nextcloud.android.sso.exceptions.AndroidGetAccountsPermissionNotGrant
import com.nextcloud.android.sso.exceptions.NextcloudFilesAppNotInstalledException;
import com.nextcloud.android.sso.helper.SingleAccountHelper;
import com.nextcloud.android.sso.model.SingleSignOnAccount;
+import com.nextcloud.android.sso.ui.UiExceptionManager;
import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.R;
@@ -36,7 +37,6 @@ import it.niedermann.nextcloud.deck.persistence.sync.SyncWorker;
import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.WrappedLiveData;
import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment;
import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler;
-import it.niedermann.nextcloud.deck.util.ExceptionUtil;
import static com.nextcloud.android.sso.AccountImporter.REQUEST_AUTH_TOKEN_SSO;
@@ -79,7 +79,11 @@ public class ImportAccountActivity extends AppCompatActivity {
try {
AccountImporter.pickNewAccount(this);
} catch (NextcloudFilesAppNotInstalledException e) {
- ExceptionUtil.handleNextcloudFilesAppNotInstalledException(this, e);
+ UiExceptionManager.showDialogForException(this, e);
+ 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/MainActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/MainActivity.java
index b5713909b..d7e726a7d 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/MainActivity.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/MainActivity.java
@@ -1,5 +1,6 @@
package it.niedermann.nextcloud.deck.ui;
+import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
@@ -53,6 +54,7 @@ import java.util.Objects;
import it.niedermann.android.crosstabdnd.CrossTabDragAndDrop;
import it.niedermann.android.tablayouthelper.TabLayoutHelper;
import it.niedermann.android.tablayouthelper.TabTitleGenerator;
+import it.niedermann.nextcloud.deck.DeckApplication;
import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.api.IResponseCallback;
@@ -65,6 +67,7 @@ import it.niedermann.nextcloud.deck.model.Stack;
import it.niedermann.nextcloud.deck.model.full.FullBoard;
import it.niedermann.nextcloud.deck.model.full.FullCard;
import it.niedermann.nextcloud.deck.model.full.FullStack;
+import it.niedermann.nextcloud.deck.model.internal.FilterInformation;
import it.niedermann.nextcloud.deck.model.ocs.Capabilities;
import it.niedermann.nextcloud.deck.model.ocs.Version;
import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
@@ -86,6 +89,7 @@ import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment;
import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler;
import it.niedermann.nextcloud.deck.ui.filter.FilterDialogFragment;
import it.niedermann.nextcloud.deck.ui.filter.FilterViewModel;
+import it.niedermann.nextcloud.deck.ui.pickstack.PickStackViewModel;
import it.niedermann.nextcloud.deck.ui.settings.SettingsActivity;
import it.niedermann.nextcloud.deck.ui.stack.DeleteStackDialogFragment;
import it.niedermann.nextcloud.deck.ui.stack.DeleteStackListener;
@@ -112,8 +116,8 @@ import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.applyBrandTo
import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.clearBrandColors;
import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.getSecondaryForegroundColorDependingOnTheme;
import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.saveBrandColors;
-import static it.niedermann.nextcloud.deck.util.ColorUtil.contrastRatioIsSufficient;
-import static it.niedermann.nextcloud.deck.util.ColorUtil.contrastRatioIsSufficientBigAreas;
+import static it.niedermann.nextcloud.deck.util.DeckColorUtil.contrastRatioIsSufficient;
+import static it.niedermann.nextcloud.deck.util.DeckColorUtil.contrastRatioIsSufficientBigAreas;
import static it.niedermann.nextcloud.deck.util.DrawerMenuUtil.MENU_ID_ABOUT;
import static it.niedermann.nextcloud.deck.util.DrawerMenuUtil.MENU_ID_ADD_BOARD;
import static it.niedermann.nextcloud.deck.util.DrawerMenuUtil.MENU_ID_ARCHIVED_BOARDS;
@@ -126,6 +130,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener
protected MainViewModel mainViewModel;
private FilterViewModel filterViewModel;
+ private PickStackViewModel pickStackViewModel;
protected static final int ACTIVITY_ABOUT = 1;
protected static final int ACTIVITY_SETTINGS = 2;
@@ -133,7 +138,6 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener
@NonNull
protected List<Account> accountsList = new ArrayList<>();
- protected SyncManager syncManager;
protected SharedPreferences sharedPreferences;
private StackAdapter stackAdapter;
long lastBoardId;
@@ -143,7 +147,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener
private Observer<List<Board>> boardsLiveDataObserver;
private Menu listMenu;
- private LiveData<List<FullStack>> stacksLiveData;
+ private LiveData<List<Stack>> stacksLiveData;
private LiveData<Boolean> hasArchivedBoardsLiveData;
private Observer<Boolean> hasArchivedBoardsLiveDataObserver;
@@ -178,6 +182,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener
mainViewModel = new ViewModelProvider(this).get(MainViewModel.class);
filterViewModel = new ViewModelProvider(this).get(FilterViewModel.class);
+ pickStackViewModel = new ViewModelProvider(this).get(PickStackViewModel.class);
addList = getString(R.string.add_list);
addBoard = getString(R.string.add_board);
@@ -191,12 +196,11 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener
toggle.syncState();
binding.navigationView.setNavigationItemSelectedListener(this);
- syncManager = new SyncManager(this);
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
- switchMap(syncManager.hasAccounts(), hasAccounts -> {
+ switchMap(mainViewModel.hasAccounts(), hasAccounts -> {
if (hasAccounts) {
- return syncManager.readAccounts();
+ return mainViewModel.readAccounts();
} else {
startActivityForResult(new Intent(this, ImportAccountActivity.class), ImportAccountActivity.REQUEST_CODE_IMPORT_ACCOUNT);
return null;
@@ -220,7 +224,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
registerAutoSyncOnNetworkAvailable();
} else {
- syncManager.synchronize(new IResponseCallback<Boolean>(mainViewModel.getCurrentAccount()) {
+ mainViewModel.synchronize(new IResponseCallback<Boolean>(mainViewModel.getCurrentAccount()) {
@Override
public void onResponse(Boolean response) {
}
@@ -240,7 +244,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener
mainViewModel.getCurrentAccountLiveData().observe(this, (currentAccount) -> {
SingleAccountHelper.setCurrentAccount(getApplicationContext(), mainViewModel.getCurrentAccount().getName());
- syncManager = new SyncManager(this);
+ mainViewModel.recreateSyncManager();
saveCurrentAccountId(this, mainViewModel.getCurrentAccount().getId());
if (mainViewModel.getCurrentAccount().isMaintenanceEnabled()) {
@@ -253,7 +257,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener
boardsLiveData.removeObserver(boardsLiveDataObserver);
}
- boardsLiveData = syncManager.getBoards(currentAccount.getId(), false);
+ boardsLiveData = mainViewModel.getBoards(currentAccount.getId(), false);
boardsLiveDataObserver = (boards) -> {
if (boards == null) {
throw new IllegalStateException("List<Board> boards must not be null.");
@@ -273,15 +277,19 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener
if (!currentBoardIdWasInList) {
setCurrentBoard(boardsList.get(0));
}
+
+ binding.filter.setOnClickListener((v) -> FilterDialogFragment.newInstance().show(getSupportFragmentManager(), EditStackDialogFragment.class.getCanonicalName()));
} else {
clearBrandColors(this);
clearCurrentBoard();
+
+ binding.filter.setOnClickListener(null);
}
if (hasArchivedBoardsLiveData != null && hasArchivedBoardsLiveDataObserver != null) {
hasArchivedBoardsLiveData.removeObserver(hasArchivedBoardsLiveDataObserver);
}
- hasArchivedBoardsLiveData = syncManager.hasArchivedBoards(currentAccount.getId());
+ hasArchivedBoardsLiveData = mainViewModel.hasArchivedBoards(currentAccount.getId());
hasArchivedBoardsLiveDataObserver = (hasArchivedBoards) -> {
mainViewModel.setCurrentAccountHasArchivedBoards(Boolean.TRUE.equals(hasArchivedBoards));
inflateBoardMenu();
@@ -320,7 +328,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener
CrossTabDragAndDrop<StackFragment, CardAdapter, FullCard> dragAndDrop = new CrossTabDragAndDrop<>(getResources(), ViewCompat.getLayoutDirection(binding.getRoot()) == ViewCompat.LAYOUT_DIRECTION_LTR);
dragAndDrop.register(binding.viewPager, binding.stackTitles, getSupportFragmentManager());
dragAndDrop.addItemMovedByDragListener((movedCard, stackId, position) -> {
- syncManager.reorder(mainViewModel.getCurrentAccount().getId(), movedCard, stackId, position);
+ mainViewModel.reorder(mainViewModel.getCurrentAccount().getId(), movedCard, stackId, position);
DeckLog.info("Card \"" + movedCard.getCard().getTitle() + "\" was moved to Stack " + stackId + " on position " + position);
});
@@ -374,8 +382,6 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener
});
filterViewModel.getFilterInformation().observe(this, (info) ->
binding.filterIndicator.setVisibility(filterViewModel.getFilterInformation().getValue() == null ? View.GONE : View.VISIBLE));
-
- binding.filter.setOnClickListener((v) -> FilterDialogFragment.newInstance().show(getSupportFragmentManager(), EditStackDialogFragment.class.getCanonicalName()));
binding.archivedCards.setOnClickListener((v) -> startActivity(ArchivedCardsActvitiy.createIntent(this, mainViewModel.getCurrentAccount(), mainViewModel.getCurrentBoardLocalId(), mainViewModel.currentBoardHasEditPermission())));
@@ -395,7 +401,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener
}
} else DeckLog.warn("ConnectivityManager is null");
refreshCapabilities(mainViewModel.getCurrentAccount());
- syncManager.synchronize(new IResponseCallback<Boolean>(mainViewModel.getCurrentAccount()) {
+ mainViewModel.synchronize(new IResponseCallback<Boolean>(mainViewModel.getCurrentAccount()) {
@Override
public void onResponse(Boolean response) {
runOnUiThread(() -> binding.swipeRefreshLayout.setRefreshing(false));
@@ -422,7 +428,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener
applyBrandToPrimaryTabLayout(mainColor, binding.stackTitles);
applyBrandToFAB(mainColor, binding.fab);
// TODO We assume, that the background of the spinner is always white
- binding.swipeRefreshLayout.setColorSchemeColors(contrastRatioIsSufficient(Color.WHITE, mainColor) ? mainColor : colorAccent);
+ binding.swipeRefreshLayout.setColorSchemeColors(contrastRatioIsSufficient(Color.WHITE, mainColor) ? mainColor : DeckApplication.isDarkTheme(this) ? Color.DKGRAY : colorAccent);
headerBinding.headerView.setBackgroundColor(mainColor);
@ColorInt final int headerTextColor = contrastRatioIsSufficientBigAreas(mainColor, Color.WHITE) ? Color.WHITE : Color.BLACK;
DrawableCompat.setTint(headerBinding.logo.getDrawable(), headerTextColor);
@@ -440,62 +446,49 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener
@Override
public void onCreateStack(String stackName) {
- // TODO this outer call is only necessary to get the highest order. Move logic to SyncManager.
- observeOnce(syncManager.getStacksForBoard(mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId()), MainActivity.this, fullStacks -> {
- final Stack s = new Stack(stackName, mainViewModel.getCurrentBoardLocalId());
- int heighestOrder = 0;
- for (FullStack fullStack : fullStacks) {
- int currentStackOrder = fullStack.stack.getOrder();
- if (currentStackOrder >= heighestOrder) {
- heighestOrder = currentStackOrder + 1;
- }
+ DeckLog.info("Create Stack in account " + mainViewModel.getCurrentAccount().getName() + " on board " + mainViewModel.getCurrentBoardLocalId());
+ WrappedLiveData<FullStack> createLiveData = mainViewModel.createStack(mainViewModel.getCurrentAccount().getId(), stackName, mainViewModel.getCurrentBoardLocalId());
+ observeOnce(createLiveData, this, (fullStack) -> {
+ if (createLiveData.hasError()) {
+ final Throwable error = createLiveData.getError();
+ assert error != null;
+ BrandedSnackbar.make(binding.coordinatorLayout, Objects.requireNonNull(error.getLocalizedMessage()), Snackbar.LENGTH_LONG)
+ .setAction(R.string.simple_more, v -> ExceptionDialogFragment.newInstance(error, mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()))
+ .show();
+ } else {
+ binding.viewPager.setCurrentItem(stackAdapter.getItemCount());
}
- s.setOrder(heighestOrder);
- DeckLog.info("Create Stack in account " + mainViewModel.getCurrentAccount().getName() + " on board " + mainViewModel.getCurrentBoardLocalId());
- WrappedLiveData<FullStack> createLiveData = syncManager.createStack(mainViewModel.getCurrentAccount().getId(), s);
- observeOnce(createLiveData, this, (fullStack) -> {
- if (createLiveData.hasError()) {
- final Throwable error = createLiveData.getError();
- assert error != null;
- BrandedSnackbar.make(binding.coordinatorLayout, Objects.requireNonNull(error.getLocalizedMessage()), Snackbar.LENGTH_LONG)
- .setAction(R.string.simple_more, v -> ExceptionDialogFragment.newInstance(error, mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()))
- .show();
- } else {
- binding.viewPager.setCurrentItem(stackAdapter.getItemCount());
- }
- });
});
}
@Override
public void onUpdateStack(long localStackId, String stackName) {
- observeOnce(syncManager.getStack(mainViewModel.getCurrentAccount().getId(), localStackId), MainActivity.this, fullStack -> {
- fullStack.getStack().setTitle(stackName);
- final WrappedLiveData<FullStack> archiveLiveData = syncManager.updateStack(fullStack);
- observeOnce(archiveLiveData, this, (v) -> {
- if (archiveLiveData.hasError()) {
- ExceptionDialogFragment.newInstance(archiveLiveData.getError(), mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
- }
- });
+ final WrappedLiveData<FullStack> liveData = mainViewModel.updateStackTitle(localStackId, stackName);
+ observeOnce(liveData, this, (v) -> {
+ if (liveData.hasError()) {
+ ExceptionDialogFragment.newInstance(liveData.getError(), mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ }
});
}
@Override
- public void onCreateBoard(String title, String color) {
+ public void onCreateBoard(String title, @ColorInt int color) {
if (boardsLiveData == null || boardsLiveDataObserver == null) {
throw new IllegalStateException("Cannot create board when noone observe boards yet. boardsLiveData or observer is null.");
}
boardsLiveData.removeObserver(boardsLiveDataObserver);
- final Board boardToCreate = new Board(title, color.startsWith("#") ? color.substring(1) : color);
+ final Board boardToCreate = new Board(title, color);
boardToCreate.setPermissionEdit(true);
boardToCreate.setPermissionManage(true);
- observeOnce(syncManager.createBoard(mainViewModel.getCurrentAccount().getId(), boardToCreate), this, createdBoard -> {
- if (createdBoard == null) {
- BrandedSnackbar.make(binding.coordinatorLayout, "Open Deck in web interface first!", Snackbar.LENGTH_LONG)
- // TODO implement action!
- // .setAction(R.string.simple_open, v -> ExceptionDialogFragment.newInstance(throwable).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()))
+
+ final WrappedLiveData<FullBoard> createLiveData = mainViewModel.createBoard(mainViewModel.getCurrentAccount().getId(), boardToCreate);
+ observeOnce(createLiveData, this, (createdBoard) -> {
+ if (createLiveData.hasError()) {
+ BrandedSnackbar.make(binding.coordinatorLayout, R.string.synchronization_failed, Snackbar.LENGTH_LONG)
+ .setAction(R.string.simple_more, v -> ExceptionDialogFragment.newInstance(createLiveData.getError(), mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()))
.show();
- } else {
+ }
+ if (createdBoard != null && !createLiveData.hasError()) {
boardsList.add(createdBoard.getBoard());
setCurrentBoard(createdBoard.getBoard());
@@ -508,11 +501,16 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener
@Override
public void onUpdateBoard(FullBoard fullBoard) {
- syncManager.updateBoard(fullBoard);
+ final WrappedLiveData<FullBoard> updateLiveData = mainViewModel.updateBoard(fullBoard);
+ observeOnce(updateLiveData, this, (next) -> {
+ if (updateLiveData.hasError()) {
+ ExceptionDialogFragment.newInstance(updateLiveData.getError(), mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ }
+ });
}
private void refreshCapabilities(final Account account) {
- syncManager.refreshCapabilities(new IResponseCallback<Capabilities>(account) {
+ mainViewModel.refreshCapabilities(new IResponseCallback<Capabilities>(account) {
@Override
public void onResponse(Capabilities response) {
if (response.isMaintenanceEnabled()) {
@@ -548,7 +546,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener
if (stacksLiveData != null) {
stacksLiveData.removeObservers(this);
}
- saveBrandColors(this, Color.parseColor('#' + board.getColor()));
+ saveBrandColors(this, board.getColor());
mainViewModel.setCurrentBoard(board);
filterViewModel.clearFilterInformation();
@@ -569,12 +567,12 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener
binding.emptyContentViewBoards.setVisibility(View.GONE);
binding.swipeRefreshLayout.setVisibility(View.VISIBLE);
- stacksLiveData = syncManager.getStacksForBoard(mainViewModel.getCurrentAccount().getId(), board.getLocalId());
- stacksLiveData.observe(this, (List<FullStack> fullStacks) -> {
- if (fullStacks == null) {
+ stacksLiveData = mainViewModel.getStacksForBoard(mainViewModel.getCurrentAccount().getId(), board.getLocalId());
+ stacksLiveData.observe(this, (List<Stack> stacks) -> {
+ if (stacks == null) {
throw new IllegalStateException("Given List<FullStack> must not be null");
}
- currentBoardStacksCount = fullStacks.size();
+ currentBoardStacksCount = stacks.size();
if (currentBoardStacksCount == 0) {
binding.emptyContentViewStacks.setVisibility(View.VISIBLE);
@@ -586,19 +584,19 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener
listMenu.findItem(R.id.archive_cards).setVisible(currentBoardHasStacks);
int stackPositionInAdapter = 0;
- stackAdapter.setStacks(fullStacks);
+ stackAdapter.setStacks(stacks);
long currentStackId = readCurrentStackId(this, mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId());
for (int i = 0; i < currentBoardStacksCount; i++) {
- if (fullStacks.get(i).getLocalId() == currentStackId || currentStackId == NO_STACK_ID) {
+ if (stacks.get(i).getLocalId() == currentStackId || currentStackId == NO_STACK_ID) {
stackPositionInAdapter = i;
break;
}
}
final int stackPositionInAdapterClone = stackPositionInAdapter;
final TabTitleGenerator tabTitleGenerator = position -> {
- if (fullStacks.size() > position) {
- return fullStacks.get(position).getStack().getTitle();
+ if (stacks.size() > position) {
+ return stacks.get(position).getTitle();
} else {
DeckLog.logError(new IllegalStateException("Could not generate tab title for position " + position + " because list size is only " + currentBoardStacksCount));
return "ERROR";
@@ -671,71 +669,67 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener
@Override
public boolean onOptionsItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- case R.id.archive_cards: {
- final FullStack fullStack = stackAdapter.getItem(binding.viewPager.getCurrentItem());
- final long stackLocalId = fullStack.getLocalId();
- observeOnce(syncManager.countCardsInStack(mainViewModel.getCurrentAccount().getId(), stackLocalId), MainActivity.this, (numberOfCards) -> {
- new BrandedAlertDialogBuilder(this)
- .setTitle(R.string.archive_cards)
- .setMessage(getString(R.string.do_you_want_to_archive_all_cards_of_the_list, fullStack.getStack().getTitle()))
- .setPositiveButton(R.string.simple_archive, (dialog, whichButton) -> {
- final WrappedLiveData<Void> archiveStackLiveData = syncManager.archiveCardsInStack(mainViewModel.getCurrentAccount().getId(), stackLocalId);
- observeOnce(archiveStackLiveData, this, (result) -> {
- if (archiveStackLiveData.hasError()) {
- ExceptionDialogFragment.newInstance(archiveStackLiveData.getError(), mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
- }
- });
- })
- .setNeutralButton(android.R.string.cancel, null)
- .create()
- .show();
- });
- return true;
- }
- case R.id.add_list: {
- EditStackDialogFragment.newInstance(NO_STACK_ID).show(getSupportFragmentManager(), addList);
- return true;
- }
- case R.id.rename_list: {
- final long stackId = stackAdapter.getItem(binding.viewPager.getCurrentItem()).getLocalId();
- observeOnce(syncManager.getStack(mainViewModel.getCurrentAccount().getId(), stackId), MainActivity.this, fullStack ->
- EditStackDialogFragment.newInstance(fullStack.getLocalId(), fullStack.getStack().getTitle())
- .show(getSupportFragmentManager(), EditStackDialogFragment.class.getCanonicalName()));
- return true;
- }
- case R.id.move_list_left: {
- final long stackId = stackAdapter.getItem(binding.viewPager.getCurrentItem()).getLocalId();
- // TODO error handling
- final int stackLeftPosition = binding.viewPager.getCurrentItem() - 1;
- final long stackLeftId = stackAdapter.getItem(stackLeftPosition).getLocalId();
- syncManager.swapStackOrder(mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId(), new Pair<>(stackId, stackLeftId));
- stackMoved = true;
- return true;
- }
- case R.id.move_list_right: {
- final long stackId = stackAdapter.getItem(binding.viewPager.getCurrentItem()).getLocalId();
- // TODO error handling
- final int stackRightPosition = binding.viewPager.getCurrentItem() + 1;
- final long stackRightId = stackAdapter.getItem(stackRightPosition).getLocalId();
- syncManager.swapStackOrder(mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId(), new Pair<>(stackId, stackRightId));
- stackMoved = true;
- return true;
- }
- case R.id.delete_list: {
- final long stackId = stackAdapter.getItem(binding.viewPager.getCurrentItem()).getLocalId();
- observeOnce(syncManager.countCardsInStack(mainViewModel.getCurrentAccount().getId(), stackId), MainActivity.this, (numberOfCards) -> {
- if (numberOfCards != null && numberOfCards > 0) {
- DeleteStackDialogFragment.newInstance(stackId, numberOfCards).show(getSupportFragmentManager(), DeleteStackDialogFragment.class.getCanonicalName());
- } else {
- onStackDeleted(stackId);
- }
- });
- return true;
- }
- default:
- return super.onOptionsItemSelected(item);
+ int itemId = item.getItemId();
+ if (itemId == R.id.archive_cards) {
+ final Stack stack = stackAdapter.getItem(binding.viewPager.getCurrentItem());
+ final long stackLocalId = stack.getLocalId();
+ observeOnce(mainViewModel.countCardsInStack(mainViewModel.getCurrentAccount().getId(), stackLocalId), MainActivity.this, (numberOfCards) -> {
+ new BrandedAlertDialogBuilder(this)
+ .setTitle(R.string.archive_cards)
+ .setMessage(getString(FilterInformation.hasActiveFilter(filterViewModel.getFilterInformation().getValue())
+ ? R.string.do_you_want_to_archive_all_cards_of_the_filtered_list
+ : R.string.do_you_want_to_archive_all_cards_of_the_list, stack.getTitle()))
+ .setPositiveButton(R.string.simple_archive, (dialog, whichButton) -> {
+ final FilterInformation filterInformation = filterViewModel.getFilterInformation().getValue();
+ final WrappedLiveData<Void> archiveStackLiveData = mainViewModel.archiveCardsInStack(mainViewModel.getCurrentAccount().getId(), stackLocalId, filterInformation == null ? new FilterInformation() : filterInformation);
+ observeOnce(archiveStackLiveData, this, (result) -> {
+ if (archiveStackLiveData.hasError() && !SyncManager.ignoreExceptionOnVoidError(archiveStackLiveData.getError())) {
+ ExceptionDialogFragment.newInstance(archiveStackLiveData.getError(), mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ }
+ });
+ })
+ .setNeutralButton(android.R.string.cancel, null)
+ .create()
+ .show();
+ });
+ return true;
+ } else if (itemId == R.id.add_list) {
+ EditStackDialogFragment.newInstance(NO_STACK_ID).show(getSupportFragmentManager(), addList);
+ return true;
+ } else if (itemId == R.id.rename_list) {
+ final long stackId = stackAdapter.getItem(binding.viewPager.getCurrentItem()).getLocalId();
+ observeOnce(mainViewModel.getStack(mainViewModel.getCurrentAccount().getId(), stackId), MainActivity.this, fullStack ->
+ EditStackDialogFragment.newInstance(fullStack.getLocalId(), fullStack.getStack().getTitle())
+ .show(getSupportFragmentManager(), EditStackDialogFragment.class.getCanonicalName()));
+ return true;
+ } else if (itemId == R.id.move_list_left) {
+ final long stackId = stackAdapter.getItem(binding.viewPager.getCurrentItem()).getLocalId();
+ // TODO error handling
+ final int stackLeftPosition = binding.viewPager.getCurrentItem() - 1;
+ final long stackLeftId = stackAdapter.getItem(stackLeftPosition).getLocalId();
+ mainViewModel.swapStackOrder(mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId(), new Pair<>(stackId, stackLeftId));
+ stackMoved = true;
+ return true;
+ } else if (itemId == R.id.move_list_right) {
+ final long stackId = stackAdapter.getItem(binding.viewPager.getCurrentItem()).getLocalId();
+ // TODO error handling
+ final int stackRightPosition = binding.viewPager.getCurrentItem() + 1;
+ final long stackRightId = stackAdapter.getItem(stackRightPosition).getLocalId();
+ mainViewModel.swapStackOrder(mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId(), new Pair<>(stackId, stackRightId));
+ stackMoved = true;
+ return true;
+ } else if (itemId == R.id.delete_list) {
+ final long stackId = stackAdapter.getItem(binding.viewPager.getCurrentItem()).getLocalId();
+ observeOnce(mainViewModel.countCardsInStack(mainViewModel.getCurrentAccount().getId(), stackId), MainActivity.this, (numberOfCards) -> {
+ if (numberOfCards != null && numberOfCards > 0) {
+ DeleteStackDialogFragment.newInstance(stackId, numberOfCards).show(getSupportFragmentManager(), DeleteStackDialogFragment.class.getCanonicalName());
+ } else {
+ onStackDeleted(stackId);
+ }
+ });
+ return true;
}
+ return super.onOptionsItemSelected(item);
}
protected void showFabIfEditPermissionGranted() {
@@ -780,7 +774,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener
default:
try {
AccountImporter.onActivityResult(requestCode, resultCode, data, this, (account) -> {
- final WrappedLiveData<Account> accountLiveData = this.syncManager.createAccount(new Account(account.name, account.userId, account.url));
+ final WrappedLiveData<Account> accountLiveData = mainViewModel.createAccount(new Account(account.name, account.userId, account.url));
accountLiveData.observe(this, (createdAccount) -> {
if (!accountLiveData.hasError()) {
if (createdAccount == null) {
@@ -789,12 +783,13 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener
final SyncManager importSyncManager = new SyncManager(this, account.name);
importSyncManager.refreshCapabilities(new IResponseCallback<Capabilities>(createdAccount) {
+ @SuppressLint("StringFormatInvalid")
@Override
public void onResponse(Capabilities response) {
if (!response.isMaintenanceEnabled()) {
if (response.getDeckVersion().isSupported(getApplicationContext())) {
runOnUiThread(() -> {
- syncManager = importSyncManager;
+ mainViewModel.setSyncManager(importSyncManager);
mainViewModel.setCurrentAccount(account);
final Snackbar importSnackbar = BrandedSnackbar.make(binding.coordinatorLayout, R.string.account_is_getting_imported, Snackbar.LENGTH_INDEFINITE);
@@ -827,7 +822,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener
startActivity(openURL);
finish();
}).show());
- syncManager.deleteAccount(createdAccount.getId());
+ mainViewModel.deleteAccount(createdAccount.getId());
}
} else {
DeckLog.warn("Cannot import account because server version is currently in maintenance mode.");
@@ -836,14 +831,14 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener
.setMessage(getString(R.string.maintenance_mode_explanation, createdAccount.getUrl()))
.setPositiveButton(R.string.simple_close, null)
.show());
- syncManager.deleteAccount(createdAccount.getId());
+ mainViewModel.deleteAccount(createdAccount.getId());
}
}
@Override
public void onError(Throwable throwable) {
super.onError(throwable);
- syncManager.deleteAccount(createdAccount.getId());
+ mainViewModel.deleteAccount(createdAccount.getId());
if (throwable instanceof OfflineException) {
DeckLog.warn("Cannot import account because device is currently offline.");
runOnUiThread(() -> new BrandedAlertDialogBuilder(MainActivity.this)
@@ -893,7 +888,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener
@Override
public void onAvailable(@NonNull Network network) {
DeckLog.log("Got Network connection");
- syncManager.synchronize(new IResponseCallback<Boolean>(mainViewModel.getCurrentAccount()) {
+ mainViewModel.synchronize(new IResponseCallback<Boolean>(mainViewModel.getCurrentAccount()) {
@Override
public void onResponse(Boolean response) {
DeckLog.log("Auto-Sync after connection available successful");
@@ -924,9 +919,9 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener
@Override
public void onStackDeleted(Long stackLocalId) {
long stackId = stackAdapter.getItem(binding.viewPager.getCurrentItem()).getLocalId();
- final WrappedLiveData<Void> deleteStackLiveData = syncManager.deleteStack(mainViewModel.getCurrentAccount().getId(), stackId, mainViewModel.getCurrentBoardLocalId());
+ final WrappedLiveData<Void> deleteStackLiveData = mainViewModel.deleteStack(mainViewModel.getCurrentAccount().getId(), stackId, mainViewModel.getCurrentBoardLocalId());
observeOnce(deleteStackLiveData, this, (v) -> {
- if (deleteStackLiveData.hasError()) {
+ if (deleteStackLiveData.hasError() && !SyncManager.ignoreExceptionOnVoidError(deleteStackLiveData.getError())) {
ExceptionDialogFragment.newInstance(deleteStackLiveData.getError(), mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
}
});
@@ -946,7 +941,14 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener
EditBoardDialogFragment.newInstance().show(getSupportFragmentManager(), addBoard);
}
}
- syncManager.deleteBoard(board);
+
+ final WrappedLiveData<Void> deleteLiveData = mainViewModel.deleteBoard(board);
+ observeOnce(deleteLiveData, this, (next) -> {
+ if (deleteLiveData.hasError() && !SyncManager.ignoreExceptionOnVoidError(deleteLiveData.getError())) {
+ ExceptionDialogFragment.newInstance(deleteLiveData.getError(), mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ }
+ });
+
binding.drawerLayout.closeDrawer(GravityCompat.START);
}
@@ -966,6 +968,39 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener
@Override
public void onArchive(@NonNull Board board) {
- syncManager.archiveBoard(board);
+ final WrappedLiveData<FullBoard> liveData = mainViewModel.archiveBoard(board);
+ observeOnce(liveData, this, (fullBoard) -> {
+ if (liveData.hasError()) {
+ ExceptionDialogFragment.newInstance(liveData.getError(), mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ }
+ });
+ }
+
+ @Override
+ public void onClone(Board board) {
+ final String[] animals = {getString(R.string.clone_cards)};
+ final boolean[] checkedItems = {false};
+ new BrandedAlertDialogBuilder(this)
+ .setTitle(R.string.clone_board)
+ .setMultiChoiceItems(animals, checkedItems, (dialog, which, isChecked) -> checkedItems[0] = isChecked)
+ .setPositiveButton(R.string.simple_clone, (dialog, which) -> {
+ binding.drawerLayout.closeDrawer(GravityCompat.START);
+ final Snackbar snackbar = BrandedSnackbar.make(binding.coordinatorLayout, getString(R.string.cloning_board, board.getTitle()), Snackbar.LENGTH_INDEFINITE);
+ snackbar.show();
+ final WrappedLiveData<FullBoard> liveData = mainViewModel.cloneBoard(board.getAccountId(), board.getLocalId(), board.getAccountId(), board.getColor(), checkedItems[0]);
+ observeOnce(liveData, this, (fullBoard -> {
+ snackbar.dismiss();
+ if (liveData.hasError()) {
+ ExceptionDialogFragment.newInstance(liveData.getError(), mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ } else {
+ setCurrentBoard(fullBoard.getBoard());
+ BrandedSnackbar.make(binding.coordinatorLayout, getString(R.string.successfully_cloned_board, fullBoard.getBoard().getTitle()), Snackbar.LENGTH_LONG)
+ .setAction(R.string.edit, v -> EditBoardDialogFragment.newInstance(fullBoard.getLocalId()).show(getSupportFragmentManager(), EditBoardDialogFragment.class.getSimpleName()))
+ .show();
+ }
+ }));
+ })
+ .setNeutralButton(android.R.string.cancel, null)
+ .show();
}
} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/MainViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/MainViewModel.java
index e5bd4f482..ac244b88e 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/MainViewModel.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/MainViewModel.java
@@ -2,19 +2,42 @@ package it.niedermann.nextcloud.deck.ui;
import android.app.Application;
+import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.core.util.Pair;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
+import java.io.File;
+import java.util.List;
+
+import it.niedermann.nextcloud.deck.api.IResponseCallback;
+import it.niedermann.nextcloud.deck.model.AccessControl;
import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.deck.model.Attachment;
import it.niedermann.nextcloud.deck.model.Board;
+import it.niedermann.nextcloud.deck.model.Card;
+import it.niedermann.nextcloud.deck.model.Label;
+import it.niedermann.nextcloud.deck.model.Stack;
+import it.niedermann.nextcloud.deck.model.User;
+import it.niedermann.nextcloud.deck.model.full.FullBoard;
+import it.niedermann.nextcloud.deck.model.full.FullCard;
+import it.niedermann.nextcloud.deck.model.full.FullStack;
+import it.niedermann.nextcloud.deck.model.internal.FilterInformation;
+import it.niedermann.nextcloud.deck.model.ocs.Capabilities;
+import it.niedermann.nextcloud.deck.model.ocs.comment.DeckComment;
+import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
+import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.WrappedLiveData;
@SuppressWarnings("WeakerAccess")
public class MainViewModel extends AndroidViewModel {
- private MutableLiveData<Account> currentAccount = new MutableLiveData<>();
+ private SyncManager syncManager;
+
+ private final MutableLiveData<Account> currentAccount = new MutableLiveData<>();
+ @Nullable
private Board currentBoard;
private boolean currentAccountHasArchivedBoards = false;
@@ -22,6 +45,7 @@ public class MainViewModel extends AndroidViewModel {
public MainViewModel(@NonNull Application application) {
super(application);
+ this.syncManager = new SyncManager(application);
}
public Account getCurrentAccount() {
@@ -37,16 +61,21 @@ public class MainViewModel extends AndroidViewModel {
this.currentAccountIsSupportedVersion = currentAccount.getServerDeckVersionAsObject().isSupported(getApplication().getApplicationContext());
}
- public void setCurrentBoard(Board currentBoard) {
+ public void setCurrentBoard(@NonNull Board currentBoard) {
this.currentBoard = currentBoard;
}
public Long getCurrentBoardLocalId() {
+ if (currentBoard == null) {
+ throw new IllegalStateException("getCurrentBoardLocalId() called before setCurrentBoard()");
+ }
return this.currentBoard.getLocalId();
}
- @Nullable
public Long getCurrentBoardRemoteId() {
+ if (currentBoard == null) {
+ throw new IllegalStateException("getCurrentBoardRemoteId() called before setCurrentBoard()");
+ }
return this.currentBoard.getId();
}
@@ -65,4 +94,192 @@ public class MainViewModel extends AndroidViewModel {
public boolean isCurrentAccountIsSupportedVersion() {
return currentAccountIsSupportedVersion;
}
+
+ public void recreateSyncManager() {
+ this.syncManager = new SyncManager(getApplication());
+ }
+
+ public void setSyncManager(@NonNull SyncManager syncManager) {
+ this.syncManager = syncManager;
+ }
+
+ public void synchronize(@NonNull IResponseCallback<Boolean> responseCallback) {
+ syncManager.synchronize(responseCallback);
+ }
+
+ public void refreshCapabilities(@NonNull IResponseCallback<Capabilities> callback) {
+ syncManager.refreshCapabilities(callback);
+ }
+
+ public LiveData<Boolean> hasAccounts() {
+ return syncManager.hasAccounts();
+ }
+
+ public WrappedLiveData<Account> createAccount(@NonNull Account accout) {
+ return syncManager.createAccount(accout);
+ }
+
+ public void deleteAccount(long id) {
+ syncManager.deleteAccount(id);
+ }
+
+ public LiveData<List<Account>> readAccounts() {
+ return syncManager.readAccounts();
+ }
+
+ public WrappedLiveData<FullBoard> createBoard(long accountId, @NonNull Board board) {
+ return syncManager.createBoard(accountId, board);
+ }
+
+ public WrappedLiveData<FullBoard> updateBoard(@NonNull FullBoard board) {
+ return syncManager.updateBoard(board);
+ }
+
+ public LiveData<List<Board>> getBoards(long accountId, boolean archived) {
+ return syncManager.getBoards(accountId, archived);
+ }
+
+ public LiveData<FullBoard> getFullBoardById(Long accountId, Long localId) {
+ return syncManager.getFullBoardById(accountId, localId);
+ }
+
+ public WrappedLiveData<FullBoard> archiveBoard(@NonNull Board board) {
+ return syncManager.archiveBoard(board);
+ }
+
+ public WrappedLiveData<FullBoard> dearchiveBoard(@NonNull Board board) {
+ return syncManager.dearchiveBoard(board);
+ }
+
+ public WrappedLiveData<FullBoard> cloneBoard(long originAccountId, long originBoardLocalId, long targetAccountId, @ColorInt int targetBoardColor, boolean cloneCards) {
+ return syncManager.cloneBoard(originAccountId, originBoardLocalId, targetAccountId, targetBoardColor, cloneCards);
+ }
+
+ public WrappedLiveData<Void> deleteBoard(@NonNull Board board) {
+ return syncManager.deleteBoard(board);
+ }
+
+ public LiveData<Boolean> hasArchivedBoards(long accountId) {
+ return syncManager.hasArchivedBoards(accountId);
+ }
+
+ public WrappedLiveData<AccessControl> createAccessControl(long accountId, AccessControl entity) {
+ return syncManager.createAccessControl(accountId, entity);
+ }
+
+ public WrappedLiveData<AccessControl> updateAccessControl(@NonNull AccessControl entity) {
+ return syncManager.updateAccessControl(entity);
+ }
+
+ public LiveData<List<AccessControl>> getAccessControlByLocalBoardId(long accountId, Long id) {
+ return syncManager.getAccessControlByLocalBoardId(accountId, id);
+ }
+
+ public WrappedLiveData<Void> deleteAccessControl(@NonNull AccessControl entity) {
+ return syncManager.deleteAccessControl(entity);
+ }
+
+ public WrappedLiveData<Label> createLabel(long accountId, Label label, long localBoardId) {
+ return syncManager.createLabel(accountId, label, localBoardId);
+ }
+
+ public LiveData<Integer> countCardsWithLabel(long localLabelId) {
+ return syncManager.countCardsWithLabel(localLabelId);
+ }
+
+ public WrappedLiveData<Label> updateLabel(@NonNull Label label) {
+ return syncManager.updateLabel(label);
+ }
+
+ public WrappedLiveData<Void> deleteLabel(@NonNull Label label) {
+ return syncManager.deleteLabel(label);
+ }
+
+ public LiveData<List<Stack>> getStacksForBoard(long accountId, long localBoardId) {
+ return syncManager.getStacksForBoard(accountId, localBoardId);
+ }
+
+ public WrappedLiveData<FullStack> createStack(long accountId, @NonNull String title, long boardLocalId) {
+ return syncManager.createStack(accountId, title, boardLocalId);
+ }
+
+ public LiveData<FullStack> getStack(long accountId, long localStackId) {
+ return syncManager.getStack(accountId, localStackId);
+ }
+
+ public void swapStackOrder(long accountId, long boardLocalId, @NonNull Pair<Long, Long> stackLocalIds) {
+ syncManager.swapStackOrder(accountId, boardLocalId, stackLocalIds);
+ }
+
+ public WrappedLiveData<FullStack> updateStackTitle(long localStackId, @NonNull String newTitle) {
+ return syncManager.updateStackTitle(localStackId, newTitle);
+ }
+
+ public WrappedLiveData<Void> deleteStack(long accountId, long stackLocalId, long boardLocalId) {
+ return syncManager.deleteStack(accountId, stackLocalId, boardLocalId);
+ }
+
+ public void reorder(long accountId, @NonNull FullCard movedCard, long newStackId, int newIndex) {
+ syncManager.reorder(accountId, movedCard, newStackId, newIndex);
+ }
+
+ public LiveData<Integer> countCardsInStack(long accountId, long localStackId) {
+ return syncManager.countCardsInStack(accountId, localStackId);
+ }
+
+ public WrappedLiveData<Void> archiveCardsInStack(long accountId, long stackLocalId, @NonNull FilterInformation filterInformation) {
+ return syncManager.archiveCardsInStack(accountId, stackLocalId, filterInformation);
+ }
+
+ public WrappedLiveData<FullCard> updateCard(@NonNull FullCard fullCard) {
+ return syncManager.updateCard(fullCard);
+ }
+
+ public void addCommentToCard(long accountId, long cardId, @NonNull DeckComment comment) {
+ syncManager.addCommentToCard(accountId, cardId, comment);
+ }
+
+ public WrappedLiveData<Attachment> addAttachmentToCard(long accountId, long localCardId, @NonNull String mimeType, @NonNull File file) {
+ return syncManager.addAttachmentToCard(accountId, localCardId, mimeType, file);
+ }
+
+ public void addOrUpdateSingleCardWidget(int widgetId, long accountId, long boardId, long localCardId) {
+ syncManager.addOrUpdateSingleCardWidget(widgetId, accountId, boardId, localCardId);
+ }
+
+ public LiveData<List<FullCard>> getFullCardsForStack(long accountId, long localStackId, @Nullable FilterInformation filter) {
+ return syncManager.getFullCardsForStack(accountId, localStackId, filter);
+ }
+
+ public WrappedLiveData<Void> moveCard(long originAccountId, long originCardLocalId, long targetAccountId, long targetBoardLocalId, long targetStackLocalId) {
+ return syncManager.moveCard(originAccountId, originCardLocalId, targetAccountId, targetBoardLocalId, targetStackLocalId);
+ }
+
+ public LiveData<List<FullCard>> getArchivedFullCardsForBoard(long accountId, long localBoardId) {
+ return syncManager.getArchivedFullCardsForBoard(accountId, localBoardId);
+ }
+
+ public void assignUserToCard(@NonNull User user, @NonNull Card card) {
+ syncManager.assignUserToCard(user, card);
+ }
+
+ public void unassignUserFromCard(@NonNull User user, @NonNull Card card) {
+ syncManager.unassignUserFromCard(user, card);
+ }
+
+ public User getUserByUidDirectly(long accountId, String uid) {
+ return syncManager.getUserByUidDirectly(accountId, uid);
+ }
+
+ public WrappedLiveData<FullCard> archiveCard(@NonNull FullCard card) {
+ return syncManager.archiveCard(card);
+ }
+
+ public WrappedLiveData<FullCard> dearchiveCard(@NonNull FullCard card) {
+ return syncManager.dearchiveCard(card);
+ }
+
+ public WrappedLiveData<Void> deleteCard(@NonNull Card card) {
+ return syncManager.deleteCard(card);
+ }
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/PickStackActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/PickStackActivity.java
new file mode 100644
index 000000000..2339a8783
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/PickStackActivity.java
@@ -0,0 +1,115 @@
+package it.niedermann.nextcloud.deck.ui;
+
+import android.content.Intent;
+import android.content.res.ColorStateList;
+import android.graphics.Color;
+import android.os.Bundle;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.content.ContextCompat;
+import androidx.core.graphics.drawable.DrawableCompat;
+import androidx.lifecycle.ViewModelProvider;
+
+import java.util.List;
+
+import it.niedermann.android.util.ColorUtil;
+import it.niedermann.nextcloud.deck.DeckLog;
+import it.niedermann.nextcloud.deck.R;
+import it.niedermann.nextcloud.deck.databinding.ActivityPickStackBinding;
+import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.deck.model.Board;
+import it.niedermann.nextcloud.deck.model.Stack;
+import it.niedermann.nextcloud.deck.ui.branding.Branded;
+import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler;
+import it.niedermann.nextcloud.deck.ui.pickstack.PickStackFragment;
+import it.niedermann.nextcloud.deck.ui.pickstack.PickStackListener;
+import it.niedermann.nextcloud.deck.ui.pickstack.PickStackViewModel;
+
+import static androidx.lifecycle.Transformations.switchMap;
+import static it.niedermann.nextcloud.deck.DeckApplication.isDarkTheme;
+import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.getSecondaryForegroundColorDependingOnTheme;
+import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.isBrandingEnabled;
+import static it.niedermann.nextcloud.deck.util.DeckColorUtil.contrastRatioIsSufficientBigAreas;
+
+public abstract class PickStackActivity extends AppCompatActivity implements Branded, PickStackListener {
+
+ protected ActivityPickStackBinding binding;
+ protected PickStackViewModel viewModel;
+
+ private boolean brandingEnabled;
+
+ private Account selectedAccount;
+ private Board selectedBoard;
+ private Stack selectedStack;
+
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ Thread.setDefaultUncaughtExceptionHandler(new ExceptionHandler(this));
+
+ brandingEnabled = isBrandingEnabled(this);
+
+ binding = ActivityPickStackBinding.inflate(getLayoutInflater());
+ viewModel = new ViewModelProvider(this).get(PickStackViewModel.class);
+
+ setContentView(binding.getRoot());
+ setSupportActionBar(binding.toolbar);
+
+ switchMap(viewModel.hasAccounts(), hasAccounts -> {
+ if (hasAccounts) {
+ return viewModel.readAccounts();
+ } else {
+ startActivityForResult(new Intent(this, ImportAccountActivity.class), ImportAccountActivity.REQUEST_CODE_IMPORT_ACCOUNT);
+ return null;
+ }
+ }).observe(this, (List<Account> accounts) -> {
+ if (accounts == null || accounts.size() == 0) {
+ throw new IllegalStateException("hasAccounts() returns true, but readAccounts() returns null or has no entry");
+ }
+ getSupportFragmentManager()
+ .beginTransaction()
+ .add(R.id.fragment_container, PickStackFragment.newInstance(showBoardsWithoutEditPermission()))
+ .commit();
+ });
+ binding.cancel.setOnClickListener((v) -> finish());
+ binding.submit.setOnClickListener((v) -> onSubmit(selectedAccount, selectedBoard.getLocalId(), selectedStack.getLocalId()));
+ }
+
+ @Override
+ public void onStackPicked(@NonNull Account account, @Nullable Board board, @Nullable Stack stack) {
+ this.selectedAccount = account;
+ this.selectedBoard = board;
+ this.selectedStack = stack;
+ if (board == null) {
+ binding.submit.setEnabled(false);
+ } else {
+ applyBrand(board.getColor());
+ binding.submit.setEnabled(stack != null);
+ }
+ }
+
+ @Override
+ public void applyBrand(int mainColor) {
+ try {
+ if (brandingEnabled) {
+ @ColorInt final int finalMainColor = contrastRatioIsSufficientBigAreas(mainColor, ContextCompat.getColor(this, R.color.primary))
+ ? mainColor
+ : isDarkTheme(this) ? Color.WHITE : Color.BLACK;
+ DrawableCompat.setTintList(binding.submit.getBackground(), ColorStateList.valueOf(finalMainColor));
+ binding.submit.setTextColor(ColorUtil.INSTANCE.getForegroundColorForBackgroundColor(finalMainColor));
+ binding.cancel.setTextColor(getSecondaryForegroundColorDependingOnTheme(this, mainColor));
+ }
+ } catch (Throwable t) {
+ DeckLog.logError(t);
+ }
+ }
+
+ abstract protected void onSubmit(Account account, long boardId, long stackId);
+
+ abstract protected boolean showBoardsWithoutEditPermission();
+} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/PushNotificationActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/PushNotificationActivity.java
index b0b0d68ae..cdc20ed50 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/PushNotificationActivity.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/PushNotificationActivity.java
@@ -5,26 +5,28 @@ import android.net.Uri;
import android.text.TextUtils;
import android.view.View;
+import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.UiThread;
import androidx.appcompat.app.AppCompatActivity;
+import androidx.lifecycle.ViewModelProvider;
-import com.nextcloud.android.sso.helper.SingleAccountHelper;
-
+import it.niedermann.android.util.ColorUtil;
import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.R;
+import it.niedermann.nextcloud.deck.api.IResponseCallback;
import it.niedermann.nextcloud.deck.databinding.ActivityPushNotificationBinding;
import it.niedermann.nextcloud.deck.model.Account;
-import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
import it.niedermann.nextcloud.deck.ui.card.EditActivity;
import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler;
+import it.niedermann.nextcloud.deck.util.ProjectUtil;
-import static android.graphics.Color.parseColor;
import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce;
public class PushNotificationActivity extends AppCompatActivity {
private ActivityPushNotificationBinding binding;
+ private PushNotificationViewModel viewModel;
// Provided by Files app NotificationJob
private static final String KEY_SUBJECT = "subject";
@@ -44,6 +46,8 @@ public class PushNotificationActivity extends AppCompatActivity {
}
binding = ActivityPushNotificationBinding.inflate(getLayoutInflater());
+ viewModel = new ViewModelProvider(this).get(PushNotificationViewModel.class);
+
setContentView(binding.getRoot());
setSupportActionBar(binding.toolbar);
@@ -56,62 +60,105 @@ public class PushNotificationActivity extends AppCompatActivity {
}
final String link = getIntent().getStringExtra(KEY_LINK);
+ long[] ids = ProjectUtil.extractBoardIdAndCardIdFromUrl(link);
binding.cancel.setOnClickListener((v) -> finish());
- final SyncManager accountReadingSyncManager = new SyncManager(this);
final String cardRemoteIdString = getIntent().getStringExtra(KEY_CARD_REMOTE_ID);
final String accountString = getIntent().getStringExtra(KEY_ACCOUNT);
DeckLog.verbose("cardRemoteIdString = " + cardRemoteIdString);
- if (cardRemoteIdString != null) {
- try {
- final int cardRemoteId = Integer.parseInt(cardRemoteIdString);
- observeOnce(accountReadingSyncManager.readAccount(accountString), this, (account -> {
- if (account != null) {
- SingleAccountHelper.setCurrentAccount(this, account.getName());
- final SyncManager syncManager = new SyncManager(this);
- DeckLog.verbose("account: " + account);
- observeOnce(syncManager.getLocalBoardIdByCardRemoteIdAndAccount(cardRemoteId, account), PushNotificationActivity.this, (boardLocalId -> {
- DeckLog.verbose("BoardLocalId " + boardLocalId);
- if (boardLocalId != null) {
- observeOnce(syncManager.synchronizeCardByRemoteId(cardRemoteId, account), PushNotificationActivity.this, (fullCard -> {
- DeckLog.verbose("FullCard: " + fullCard);
- if (fullCard != null) {
- runOnUiThread(() -> {
- binding.submit.setOnClickListener((v) -> launchEditActivity(account, boardLocalId, fullCard.getLocalId()));
- binding.submit.setText(R.string.simple_open);
- applyBrandToSubmitButton(account);
- binding.submit.setEnabled(true);
- binding.progress.setVisibility(View.INVISIBLE);
- });
- } else {
- DeckLog.warn("Something went wrong while synchronizing the card " + cardRemoteId + " (cardRemoteId). Given fullCard is null.");
- applyBrandToSubmitButton(account);
- fallbackToBrowser(link);
- }
- }));
- } else {
- DeckLog.warn("Given localBoardId for cardRemoteId " + cardRemoteId + " is null.");
- applyBrandToSubmitButton(account);
- fallbackToBrowser(link);
- }
- }));
- } else {
- DeckLog.warn("Given account for " + accountString + " is null.");
- fallbackToBrowser(link);
- }
- }));
- } catch (NumberFormatException e) {
- DeckLog.logError(e);
+ if (ids.length == 2) {
+ if (cardRemoteIdString != null) {
+ try {
+ final int cardRemoteId = Integer.parseInt(cardRemoteIdString);
+ observeOnce(viewModel.readAccount(accountString), this, (account -> {
+ if (account != null) {
+ viewModel.setAccount(account.getName());
+ DeckLog.verbose("account: " + account);
+ observeOnce(viewModel.getBoardByRemoteId(account.getId(), ids[0]), PushNotificationActivity.this, (board -> {
+ DeckLog.verbose("BoardLocalId " + board);
+ if (board != null) {
+ observeOnce(viewModel.getCardByRemoteID(account.getId(), cardRemoteId), PushNotificationActivity.this, (card -> {
+ DeckLog.verbose("Card: " + card);
+ if (card != null) {
+ viewModel.synchronizeCard(new IResponseCallback<Boolean>(account) {
+ @Override
+ public void onResponse(Boolean response) {
+ openCardOnSubmit(account, board.getLocalId(), card.getLocalId());
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ super.onError(throwable);
+ openCardOnSubmit(account, board.getLocalId(), card.getLocalId());
+ }
+ }, card);
+ } else {
+ DeckLog.info("Card is not yet available locally. Synchronize board with localId " + board);
+
+ viewModel.synchronizeBoard(new IResponseCallback<Boolean>(account) {
+ @Override
+ public void onResponse(Boolean response) {
+ runOnUiThread(() -> {
+ observeOnce(viewModel.getCardByRemoteID(account.getId(), cardRemoteId), PushNotificationActivity.this, (card -> {
+ DeckLog.verbose("Card: " + card);
+ if (card != null) {
+ openCardOnSubmit(account, board.getLocalId(), card.getLocalId());
+ } else {
+ DeckLog.warn("Something went wrong while synchronizing the card " + cardRemoteId + " (cardRemoteId). Given fullCard is null.");
+ applyBrandToSubmitButton(account);
+ fallbackToBrowser(link);
+ }
+ }));
+ });
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ super.onError(throwable);
+ DeckLog.warn("Something went wrong while synchronizing the board with localId " + board + ".");
+ applyBrandToSubmitButton(account);
+ fallbackToBrowser(link);
+ }
+ }, board.getLocalId());
+ }
+ }));
+ } else {
+ DeckLog.warn("Given localBoardId for cardRemoteId " + cardRemoteId + " is null.");
+ applyBrandToSubmitButton(account);
+ fallbackToBrowser(link);
+ }
+ }));
+ } else {
+ DeckLog.warn("Given account for " + accountString + " is null.");
+ fallbackToBrowser(link);
+ }
+ }));
+ } catch (NumberFormatException e) {
+ DeckLog.logError(e);
+ fallbackToBrowser(link);
+ }
+ } else {
+ DeckLog.warn(KEY_CARD_REMOTE_ID + " is null.");
fallbackToBrowser(link);
}
} else {
- DeckLog.warn(KEY_CARD_REMOTE_ID + " is null.");
+ DeckLog.warn("Link does not contain two IDs (expected one board id and one card id): " + link);
fallbackToBrowser(link);
}
}
+ private void openCardOnSubmit(@NonNull Account account, long boardLocalId, long cardlocalId) {
+ runOnUiThread(() -> {
+ binding.submit.setOnClickListener((v) -> launchEditActivity(account, boardLocalId, cardlocalId));
+ binding.submit.setText(R.string.simple_open);
+ applyBrandToSubmitButton(account);
+ binding.submit.setEnabled(true);
+ binding.progress.setVisibility(View.INVISIBLE);
+ });
+ }
+
/**
* If anything goes wrong and we cannot open the card directly, we fall back to open the given link in the webbrowser
*/
@@ -146,10 +193,13 @@ public class PushNotificationActivity extends AppCompatActivity {
return true;
}
+ // TODO implement Branded interface
+ // TODO apply branding based on board color
public void applyBrandToSubmitButton(@NonNull Account account) {
+ @ColorInt final int mainColor = account.getColor();
try {
- binding.submit.setBackgroundColor(parseColor(account.getColor()));
- binding.submit.setTextColor(parseColor(account.getTextColor()));
+ binding.submit.setBackgroundColor(mainColor);
+ binding.submit.setTextColor(ColorUtil.INSTANCE.getForegroundColorForBackgroundColor(mainColor));
} catch (Throwable t) {
DeckLog.logError(t);
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/PushNotificationViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/PushNotificationViewModel.java
new file mode 100644
index 000000000..d15b412f4
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/PushNotificationViewModel.java
@@ -0,0 +1,52 @@
+package it.niedermann.nextcloud.deck.ui;
+
+import android.app.Application;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.LiveData;
+
+import com.nextcloud.android.sso.helper.SingleAccountHelper;
+
+import it.niedermann.nextcloud.deck.api.IResponseCallback;
+import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.deck.model.Board;
+import it.niedermann.nextcloud.deck.model.Card;
+import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
+
+public class PushNotificationViewModel extends AndroidViewModel {
+
+ private final SyncManager readAccountSyncManager;
+ private SyncManager accountSpecificSyncManager;
+
+ public PushNotificationViewModel(@NonNull Application application) {
+ super(application);
+ this.readAccountSyncManager = new SyncManager(application);
+ }
+
+ public LiveData<Account> readAccount(@Nullable String name) {
+ return readAccountSyncManager.readAccount(name);
+ }
+
+ public void setAccount(@NonNull String accountName) {
+ SingleAccountHelper.setCurrentAccount(getApplication(), accountName);
+ accountSpecificSyncManager = new SyncManager(getApplication());
+ }
+
+ public LiveData<Board> getBoardByRemoteId(long accountId, long remoteId) {
+ return accountSpecificSyncManager.getBoardByRemoteId(accountId, remoteId);
+ }
+
+ public LiveData<Card> getCardByRemoteID(long accountId, long remoteId) {
+ return accountSpecificSyncManager.getCardByRemoteID(accountId, remoteId);
+ }
+
+ public void synchronizeCard(@NonNull IResponseCallback<Boolean> responseCallback, Card card) {
+ accountSpecificSyncManager.synchronizeCard(responseCallback, card);
+ }
+
+ public void synchronizeBoard(@NonNull IResponseCallback<Boolean> responseCallback, long localBoadId) {
+ accountSpecificSyncManager.synchronizeBoard(responseCallback, localBoadId);
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/about/AboutFragmentLicenseTab.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/about/AboutFragmentLicenseTab.java
index c00ff212a..0bd92bc78 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/about/AboutFragmentLicenseTab.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/about/AboutFragmentLicenseTab.java
@@ -14,13 +14,13 @@ import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.drawable.DrawableCompat;
+import it.niedermann.android.util.ColorUtil;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.FragmentAboutLicenseTabBinding;
import it.niedermann.nextcloud.deck.ui.branding.BrandedFragment;
-import it.niedermann.nextcloud.deck.util.ColorUtil;
import static it.niedermann.nextcloud.deck.DeckApplication.isDarkTheme;
-import static it.niedermann.nextcloud.deck.util.ColorUtil.contrastRatioIsSufficientBigAreas;
+import static it.niedermann.nextcloud.deck.util.DeckColorUtil.contrastRatioIsSufficientBigAreas;
import static it.niedermann.nextcloud.deck.util.SpannableUtil.setTextWithURL;
public class AboutFragmentLicenseTab extends BrandedFragment {
@@ -42,6 +42,6 @@ public class AboutFragmentLicenseTab extends BrandedFragment {
? mainColor
: isDarkTheme(requireContext()) ? Color.WHITE : Color.BLACK;
DrawableCompat.setTintList(binding.aboutAppLicenseButton.getBackground(), ColorStateList.valueOf(finalMainColor));
- binding.aboutAppLicenseButton.setTextColor(ColorUtil.getForegroundColorForBackgroundColor(finalMainColor));
+ binding.aboutAppLicenseButton.setTextColor(ColorUtil.INSTANCE.getForegroundColorForBackgroundColor(finalMainColor));
}
} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherDialog.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherDialog.java
index 592f2e8cc..744498c4a 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherDialog.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherDialog.java
@@ -1,7 +1,6 @@
package it.niedermann.nextcloud.deck.ui.accountswitcher;
import android.app.Dialog;
-import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
@@ -16,43 +15,37 @@ import com.bumptech.glide.request.RequestOptions;
import com.nextcloud.android.sso.AccountImporter;
import com.nextcloud.android.sso.exceptions.AndroidGetAccountsPermissionNotGranted;
import com.nextcloud.android.sso.exceptions.NextcloudFilesAppNotInstalledException;
+import com.nextcloud.android.sso.ui.UiExceptionManager;
+import it.niedermann.android.util.DimensionUtil;
+import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.DialogAccountSwitcherBinding;
-import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
import it.niedermann.nextcloud.deck.ui.MainViewModel;
import it.niedermann.nextcloud.deck.ui.branding.BrandedDialogFragment;
import it.niedermann.nextcloud.deck.ui.manageaccounts.ManageAccountsActivity;
-import it.niedermann.nextcloud.deck.util.ExceptionUtil;
import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce;
import static it.niedermann.nextcloud.deck.ui.MainActivity.ACTIVITY_MANAGE_ACCOUNTS;
-import static it.niedermann.nextcloud.deck.util.DimensionUtil.dpToPx;
public class AccountSwitcherDialog extends BrandedDialogFragment {
private AccountSwitcherAdapter adapter;
- private SyncManager syncManager;
private DialogAccountSwitcherBinding binding;
private MainViewModel viewModel;
- @Override
- public void onAttach(@NonNull Context context) {
- super.onAttach(context);
- viewModel = new ViewModelProvider(requireActivity()).get(MainViewModel.class);
- syncManager = new SyncManager(requireActivity());
- }
-
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
binding = DialogAccountSwitcherBinding.inflate(requireActivity().getLayoutInflater());
+ viewModel = new ViewModelProvider(requireActivity()).get(MainViewModel.class);
+
binding.accountName.setText(viewModel.getCurrentAccount().getUserName());
binding.accountHost.setText(Uri.parse(viewModel.getCurrentAccount().getUrl()).getHost());
binding.check.setSelected(true);
Glide.with(requireContext())
- .load(viewModel.getCurrentAccount().getAvatarUrl(dpToPx(binding.currentAccountItemAvatar.getContext(), R.dimen.avatar_size)))
+ .load(viewModel.getCurrentAccount().getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(binding.currentAccountItemAvatar.getContext(), R.dimen.avatar_size)))
.placeholder(R.drawable.ic_baseline_account_circle_24)
.error(R.drawable.ic_baseline_account_circle_24)
.apply(RequestOptions.circleCropTransform())
@@ -65,7 +58,7 @@ public class AccountSwitcherDialog extends BrandedDialogFragment {
dismiss();
}));
- observeOnce(syncManager.readAccounts(), requireActivity(), (accounts) -> {
+ observeOnce(viewModel.readAccounts(), requireActivity(), (accounts) -> {
accounts.remove(viewModel.getCurrentAccount());
adapter.setAccounts(accounts);
});
@@ -76,7 +69,10 @@ public class AccountSwitcherDialog extends BrandedDialogFragment {
try {
AccountImporter.pickNewAccount(requireActivity());
} catch (NextcloudFilesAppNotInstalledException e) {
- ExceptionUtil.handleNextcloudFilesAppNotInstalledException(requireContext(), e);
+ UiExceptionManager.showDialogForException(requireContext(), e);
+ DeckLog.warn("=============================================================");
+ DeckLog.warn("Nextcloud app is not installed. Cannot choose account");
+ DeckLog.logError(e);
} catch (AndroidGetAccountsPermissionNotGranted e) {
AccountImporter.requestAndroidAccountPermissionsAndPickAccount(requireActivity());
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherViewHolder.java
index 9c93c422e..a60b6a0ea 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherViewHolder.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherViewHolder.java
@@ -10,12 +10,11 @@ import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.RequestOptions;
-import it.niedermann.android.glidesso.SingleSignOnUrl;
+import it.niedermann.android.util.DimensionUtil;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.ItemAccountChooseBinding;
import it.niedermann.nextcloud.deck.model.Account;
-
-import static it.niedermann.nextcloud.deck.util.DimensionUtil.dpToPx;
+import it.niedermann.nextcloud.sso.glide.SingleSignOnUrl;
public class AccountSwitcherViewHolder extends RecyclerView.ViewHolder {
@@ -30,7 +29,7 @@ public class AccountSwitcherViewHolder extends RecyclerView.ViewHolder {
binding.accountName.setText(account.getUserName());
binding.accountHost.setText(Uri.parse(account.getUrl()).getHost());
Glide.with(itemView.getContext())
- .load(new SingleSignOnUrl(account.getName(), account.getAvatarUrl(dpToPx(binding.accountItemAvatar.getContext(), R.dimen.avatar_size))))
+ .load(new SingleSignOnUrl(account.getName(), account.getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(binding.accountItemAvatar.getContext(), R.dimen.avatar_size))))
.placeholder(R.drawable.ic_baseline_account_circle_24)
.error(R.drawable.ic_baseline_account_circle_24)
.apply(RequestOptions.circleCropTransform())
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardViewHolder.java
index 5ab94b4f6..30f1e5d49 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardViewHolder.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardViewHolder.java
@@ -6,6 +6,7 @@ import android.view.MenuItem;
import android.view.View;
import androidx.appcompat.widget.PopupMenu;
+import androidx.core.content.ContextCompat;
import androidx.core.util.Consumer;
import androidx.fragment.app.FragmentManager;
import androidx.recyclerview.widget.RecyclerView;
@@ -30,14 +31,13 @@ public class ArchivedBoardViewHolder extends RecyclerView.ViewHolder {
void bind(boolean isSupportedVersion, Board board, FragmentManager fragmentManager, Consumer<Board> dearchiveBoardListener) {
final Context context = itemView.getContext();
- binding.boardIcon.setImageDrawable(ViewUtil.getTintedImageView(binding.boardIcon.getContext(), R.drawable.circle_grey600_36dp, "#" + board.getColor()));
+ binding.boardIcon.setImageDrawable(ViewUtil.getTintedImageView(binding.boardIcon.getContext(), R.drawable.circle_grey600_36dp, board.getColor()));
binding.boardMenu.setVisibility(View.GONE);
binding.boardTitle.setText(board.getTitle());
if (isSupportedVersion) {
if (board.isPermissionManage()) {
binding.boardMenu.setVisibility(View.VISIBLE);
- binding.boardMenu.setImageDrawable(ViewUtil.getTintedImageView(context, R.drawable.ic_menu, R.color.grey600));
-
+ binding.boardMenu.setImageDrawable(ViewUtil.getTintedImageView(context, R.drawable.ic_menu, ContextCompat.getColor(context, R.color.grey600)));
binding.boardMenu.setOnClickListener((v) -> {
PopupMenu popup = new PopupMenu(context, binding.boardMenu);
popup.getMenuInflater().inflate(R.menu.archived_board_menu, popup.getMenu());
@@ -47,28 +47,27 @@ public class ArchivedBoardViewHolder extends RecyclerView.ViewHolder {
}
popup.setOnMenuItemClickListener((MenuItem item) -> {
final String editBoard = context.getString(R.string.edit_board);
- switch (item.getItemId()) {
- case SHARE_BOARD_ID:
- AccessControlDialogFragment.newInstance(board.getLocalId()).show(fragmentManager, AccessControlDialogFragment.class.getSimpleName());
- return true;
- case R.id.edit_board:
- EditBoardDialogFragment.newInstance(board.getLocalId()).show(fragmentManager, editBoard);
- return true;
- case R.id.dearchive_board:
- dearchiveBoardListener.accept(board);
- return true;
- case R.id.delete_board:
- DeleteBoardDialogFragment.newInstance(board).show(fragmentManager, DeleteBoardDialogFragment.class.getSimpleName());
- return true;
- default:
- return false;
+ int itemId = item.getItemId();
+ if (itemId == SHARE_BOARD_ID) {
+ AccessControlDialogFragment.newInstance(board.getLocalId()).show(fragmentManager, AccessControlDialogFragment.class.getSimpleName());
+ return true;
+ } else if (itemId == R.id.edit_board) {
+ EditBoardDialogFragment.newInstance(board.getLocalId()).show(fragmentManager, editBoard);
+ return true;
+ } else if (itemId == R.id.dearchive_board) {
+ dearchiveBoardListener.accept(board);
+ return true;
+ } else if (itemId == R.id.delete_board) {
+ DeleteBoardDialogFragment.newInstance(board).show(fragmentManager, DeleteBoardDialogFragment.class.getSimpleName());
+ return true;
}
+ return false;
});
popup.show();
});
} else if (board.isPermissionShare()) {
binding.boardMenu.setVisibility(View.VISIBLE);
- binding.boardMenu.setImageDrawable(ViewUtil.getTintedImageView(context, R.drawable.ic_share_grey600_18dp, R.color.grey600));
+ binding.boardMenu.setImageDrawable(ViewUtil.getTintedImageView(context, R.drawable.ic_share_grey600_18dp, ContextCompat.getColor(context, R.color.grey600)));
binding.boardMenu.setOnClickListener((v) -> AccessControlDialogFragment.newInstance(board.getLocalId()).show(fragmentManager, AccessControlDialogFragment.class.getSimpleName()));
}
binding.boardMenu.setVisibility(View.VISIBLE);
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardsActvitiy.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardsActvitiy.java
index 7c3a2d23c..d6d9cac1e 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardsActvitiy.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardsActvitiy.java
@@ -15,13 +15,17 @@ import it.niedermann.nextcloud.deck.model.Account;
import it.niedermann.nextcloud.deck.model.Board;
import it.niedermann.nextcloud.deck.model.full.FullBoard;
import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
+import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.WrappedLiveData;
import it.niedermann.nextcloud.deck.ui.MainViewModel;
import it.niedermann.nextcloud.deck.ui.board.ArchiveBoardListener;
import it.niedermann.nextcloud.deck.ui.board.DeleteBoardListener;
import it.niedermann.nextcloud.deck.ui.board.EditBoardListener;
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 static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce;
+
public class ArchivedBoardsActvitiy extends BrandedActivity implements DeleteBoardListener, EditBoardListener, ArchiveBoardListener {
private static final String BUNDLE_KEY_ACCOUNT = "accountId";
@@ -29,7 +33,6 @@ public class ArchivedBoardsActvitiy extends BrandedActivity implements DeleteBoa
private MainViewModel viewModel;
private ActivityArchivedBinding binding;
private ArchivedBoardsAdapter adapter;
- private SyncManager syncManager;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
@@ -54,12 +57,18 @@ public class ArchivedBoardsActvitiy extends BrandedActivity implements DeleteBoa
viewModel = new ViewModelProvider(this).get(MainViewModel.class);
viewModel.setCurrentAccount(account);
- syncManager = new SyncManager(this);
- adapter = new ArchivedBoardsAdapter(viewModel.isCurrentAccountIsSupportedVersion(), getSupportFragmentManager(), (board) -> syncManager.dearchiveBoard(board));
+ adapter = new ArchivedBoardsAdapter(viewModel.isCurrentAccountIsSupportedVersion(), getSupportFragmentManager(), (board) -> {
+ final WrappedLiveData<FullBoard> liveData = viewModel.dearchiveBoard(board);
+ observeOnce(liveData, this, (fullBoard) -> {
+ if (liveData.hasError()) {
+ ExceptionDialogFragment.newInstance(liveData.getError(), viewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ }
+ });
+ });
binding.recyclerView.setAdapter(adapter);
- syncManager.getBoards(account.getId(), true).observe(this, (boards) -> {
+ viewModel.getBoards(account.getId(), true).observe(this, (boards) -> {
viewModel.setCurrentAccountHasArchivedBoards(boards != null && boards.size() > 0);
adapter.setBoards(boards == null ? Collections.emptyList() : boards);
});
@@ -80,16 +89,36 @@ public class ArchivedBoardsActvitiy extends BrandedActivity implements DeleteBoa
@Override
public void onBoardDeleted(Board board) {
- syncManager.deleteBoard(board);
+ final WrappedLiveData<Void> deleteLiveData = viewModel.deleteBoard(board);
+ observeOnce(deleteLiveData, this, (next) -> {
+ if (deleteLiveData.hasError() && !SyncManager.ignoreExceptionOnVoidError(deleteLiveData.getError())) {
+ ExceptionDialogFragment.newInstance(deleteLiveData.getError(), viewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ }
+ });
}
@Override
public void onUpdateBoard(FullBoard fullBoard) {
- syncManager.updateBoard(fullBoard);
+ final WrappedLiveData<FullBoard> updateLiveData = viewModel.updateBoard(fullBoard);
+ observeOnce(updateLiveData, this, (next) -> {
+ if (updateLiveData.hasError()) {
+ ExceptionDialogFragment.newInstance(updateLiveData.getError(), viewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ }
+ });
}
@Override
public void onArchive(Board board) {
- syncManager.dearchiveBoard(board);
+ final WrappedLiveData<FullBoard> liveData = viewModel.dearchiveBoard(board);
+ observeOnce(liveData, this, (fullBoard) -> {
+ if (liveData.hasError()) {
+ ExceptionDialogFragment.newInstance(liveData.getError(), viewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ }
+ });
+ }
+
+ @Override
+ public void onClone(Board board) {
+
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedcards/ArchivedCardsActvitiy.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedcards/ArchivedCardsActvitiy.java
index ed2ee7097..b3533528e 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedcards/ArchivedCardsActvitiy.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedcards/ArchivedCardsActvitiy.java
@@ -6,12 +6,15 @@ import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.lifecycle.ViewModelProvider;
import it.niedermann.nextcloud.deck.databinding.ActivityArchivedBinding;
import it.niedermann.nextcloud.deck.model.Account;
-import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
+import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper;
+import it.niedermann.nextcloud.deck.ui.MainViewModel;
import it.niedermann.nextcloud.deck.ui.branding.BrandedActivity;
import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler;
+import it.niedermann.nextcloud.deck.ui.pickstack.PickStackViewModel;
public class ArchivedCardsActvitiy extends BrandedActivity {
@@ -21,7 +24,8 @@ public class ArchivedCardsActvitiy extends BrandedActivity {
private ActivityArchivedBinding binding;
private ArchivedCardsAdapter adapter;
- private SyncManager syncManager;
+ private MainViewModel viewModel;
+ private PickStackViewModel pickStackViewModel;
private Account account;
private long boardId;
@@ -50,16 +54,20 @@ public class ArchivedCardsActvitiy extends BrandedActivity {
}
binding = ActivityArchivedBinding.inflate(getLayoutInflater());
+ viewModel = new ViewModelProvider(this).get(MainViewModel.class);
+ pickStackViewModel = new ViewModelProvider(this).get(PickStackViewModel.class);
+
setContentView(binding.getRoot());
setSupportActionBar(binding.toolbar);
- syncManager = new SyncManager(this);
+ viewModel.setCurrentAccount(account);
+ LiveDataHelper.observeOnce(viewModel.getFullBoardById(account.getId(), boardId), this, (fullBoard) -> {
+ viewModel.setCurrentBoard(fullBoard.getBoard());
- adapter = new ArchivedCardsAdapter(this, getSupportFragmentManager(), account, boardId, false, syncManager, this);
- binding.recyclerView.setAdapter(adapter);
+ adapter = new ArchivedCardsAdapter(this, getSupportFragmentManager(), viewModel, this);
+ binding.recyclerView.setAdapter(adapter);
- syncManager.getArchivedFullCardsForBoard(account.getId(), boardId).observe(this, (fullCards) -> {
- adapter.setCardList(fullCards);
+ viewModel.getArchivedFullCardsForBoard(account.getId(), boardId).observe(this, (fullCards) -> adapter.setCardList(fullCards));
});
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedcards/ArchivedCardsAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedcards/ArchivedCardsAdapter.java
index e6abf0ccc..b5034ebfa 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedcards/ArchivedCardsAdapter.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedcards/ArchivedCardsAdapter.java
@@ -1,67 +1,55 @@
package it.niedermann.nextcloud.deck.ui.archivedcards;
import android.content.Context;
-import android.view.Menu;
import android.view.MenuItem;
-import android.view.View;
-import android.widget.PopupMenu;
import androidx.annotation.NonNull;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.LifecycleOwner;
-import org.jetbrains.annotations.NotNull;
-
import it.niedermann.nextcloud.deck.R;
-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.persistence.sync.adapters.db.util.WrappedLiveData;
+import it.niedermann.nextcloud.deck.ui.MainViewModel;
+import it.niedermann.nextcloud.deck.ui.card.AbstractCardViewHolder;
import it.niedermann.nextcloud.deck.ui.card.CardAdapter;
-import it.niedermann.nextcloud.deck.ui.card.ItemCardViewHolder;
+import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment;
+
+import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce;
public class ArchivedCardsAdapter extends CardAdapter {
@SuppressWarnings("WeakerAccess")
- public ArchivedCardsAdapter(@NonNull Context context, @NonNull FragmentManager fragmentManager, @NonNull Account account, long boardId, boolean canEdit, @NonNull SyncManager syncManager, @NonNull LifecycleOwner lifecycleOwner) {
- super(context, fragmentManager, account, boardId, 0L, 0L, canEdit, syncManager, lifecycleOwner, null);
+ public ArchivedCardsAdapter(@NonNull Context context, @NonNull FragmentManager fragmentManager, @NonNull MainViewModel viewModel, @NonNull LifecycleOwner lifecycleOwner) {
+ super(context, fragmentManager, 0L, viewModel, lifecycleOwner, null);
}
@Override
- public void onBindViewHolder(@NonNull ItemCardViewHolder viewHolder, int position) {
- super.onBindViewHolder(viewHolder, position);
- viewHolder.binding.card.setOnClickListener(null);
- viewHolder.binding.card.setOnLongClickListener(null);
- }
-
- protected void onOverflowIconClicked(@NotNull View view, FullCard card) {
- final Context context = view.getContext();
- final PopupMenu popup = new PopupMenu(context, view);
- popup.inflate(R.menu.card_menu);
- prepareOptionsMenu(popup.getMenu(), card);
-
- popup.setOnMenuItemClickListener(item -> optionsItemSelected(context, item, card));
- popup.show();
- }
-
- protected void prepareOptionsMenu(Menu menu, @NotNull FullCard card) {
- // Nothing to do
+ public void onBindViewHolder(@NonNull AbstractCardViewHolder viewHolder, int position) {
+ viewHolder.bind(cardList.get(position), mainViewModel.getCurrentAccount(), mainViewModel.getCurrentBoardRemoteId(), false, R.menu.archived_card_menu, this, counterMaxValue, mainColor);
}
- protected boolean optionsItemSelected(@NonNull Context context, @NotNull MenuItem item, FullCard fullCard) {
- switch (item.getItemId()) {
- case R.id.action_card_dearchive: {
- // TODO error handling
- new Thread(() -> syncManager.dearchiveCard(fullCard)).start();
- return true;
- }
- case R.id.action_card_delete: {
- // TODO error handling
- syncManager.deleteCard(fullCard.getCard());
- return true;
- }
- default: {
- return false;
- }
+ @Override
+ public boolean onCardOptionsItemSelected(@NonNull MenuItem menuItem, @NonNull FullCard fullCard) {
+ int itemId = menuItem.getItemId();
+ if (itemId == R.id.action_card_dearchive) {
+ final WrappedLiveData<FullCard> liveData = mainViewModel.dearchiveCard(fullCard);
+ observeOnce(liveData, lifecycleOwner, (next) -> {
+ if (liveData.hasError()) {
+ ExceptionDialogFragment.newInstance(liveData.getError(), mainViewModel.getCurrentAccount()).show(fragmentManager, ExceptionDialogFragment.class.getSimpleName());
+ }
+ });
+ return true;
+ } else if (itemId == R.id.action_card_delete) {
+ final WrappedLiveData<Void> liveData = mainViewModel.deleteCard(fullCard.getCard());
+ observeOnce(liveData, lifecycleOwner, (next) -> {
+ if (liveData.hasError() && !SyncManager.ignoreExceptionOnVoidError(liveData.getError())) {
+ ExceptionDialogFragment.newInstance(liveData.getError(), mainViewModel.getCurrentAccount()).show(fragmentManager, ExceptionDialogFragment.class.getSimpleName());
+ }
+ });
+ return true;
}
+ return super.onCardOptionsItemSelected(menuItem, fullCard);
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentAdapter.java
index 0794323ec..c7d32bd37 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentAdapter.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentAdapter.java
@@ -1,43 +1,31 @@
package it.niedermann.nextcloud.deck.ui.attachments;
import android.content.Context;
-import android.graphics.drawable.Drawable;
-import android.os.Build;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.fragment.app.FragmentActivity;
import androidx.recyclerview.widget.RecyclerView;
-import com.bumptech.glide.Glide;
-import com.bumptech.glide.load.DataSource;
-import com.bumptech.glide.load.engine.GlideException;
-import com.bumptech.glide.request.RequestListener;
-import com.bumptech.glide.request.target.Target;
-
+import java.util.ArrayList;
import java.util.List;
-import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.ItemAttachmentBinding;
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.MimeTypeUtil;
public class AttachmentAdapter extends RecyclerView.Adapter<AttachmentViewHolder> {
private final Account account;
private final long cardRemoteId;
@NonNull
- private List<Attachment> attachments;
- private Context context;
+ private final List<Attachment> attachments = new ArrayList<>();
@SuppressWarnings("WeakerAccess")
public AttachmentAdapter(@NonNull Account account, long cardRemoteId, @NonNull List<Attachment> attachments) {
super();
- this.attachments = attachments;
+ this.attachments.clear();
+ this.attachments.addAll(attachments);
this.account = account;
this.cardRemoteId = cardRemoteId;
}
@@ -45,43 +33,13 @@ public class AttachmentAdapter extends RecyclerView.Adapter<AttachmentViewHolder
@NonNull
@Override
public AttachmentViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
- this.context = parent.getContext();
- return new AttachmentViewHolder(ItemAttachmentBinding.inflate(LayoutInflater.from(context), parent, false));
+ final Context context = parent.getContext();
+ return new AttachmentViewHolder(context, ItemAttachmentBinding.inflate(LayoutInflater.from(context), parent, false));
}
@Override
public void onBindViewHolder(@NonNull AttachmentViewHolder holder, int position) {
- final Attachment attachment = attachments.get(position);
- final String uri = AttachmentUtil.getRemoteUrl(account.getUrl(), cardRemoteId, attachment.getId());
- if (MimeTypeUtil.isImage(attachment.getMimetype())) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- holder.binding.preview.setTransitionName(context.getString(R.string.transition_attachment_preview, String.valueOf(attachment.getLocalId())));
- }
- holder.binding.preview.setImageResource(R.drawable.ic_image_grey600_24dp);
- Glide.with(context)
- .load(uri)
- .listener(new RequestListener<Drawable>() {
- @Override
- public boolean onLoadFailed(@Nullable GlideException e, Object model,
- Target<Drawable> target, boolean isFirstResource) {
- if (context instanceof FragmentActivity) {
- ((FragmentActivity) context).supportStartPostponedEnterTransition();
- }
- return false;
- }
-
- @Override
- public boolean onResourceReady(Drawable resource, Object model,
- Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
- if (context instanceof FragmentActivity) {
- ((FragmentActivity) context).supportStartPostponedEnterTransition();
- }
- return false;
- }
- })
- .error(R.drawable.ic_image_grey600_24dp)
- .into(holder.binding.preview);
- }
+ holder.bind(account, attachments.get(position), cardRemoteId);
}
@Override
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentViewHolder.java
index 584a57d1d..6f4fe3c74 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentViewHolder.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentViewHolder.java
@@ -1,15 +1,72 @@
package it.niedermann.nextcloud.deck.ui.attachments;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.FragmentActivity;
import androidx.recyclerview.widget.RecyclerView;
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.load.DataSource;
+import com.bumptech.glide.load.engine.GlideException;
+import com.bumptech.glide.request.RequestListener;
+import com.bumptech.glide.request.target.Target;
+
+import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.ItemAttachmentBinding;
+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.MimeTypeUtil;
public class AttachmentViewHolder extends RecyclerView.ViewHolder {
- public ItemAttachmentBinding binding;
+ @NonNull
+ private final Context parentContext;
+ @NonNull
+ private final ItemAttachmentBinding binding;
@SuppressWarnings("WeakerAccess")
- public AttachmentViewHolder(ItemAttachmentBinding binding) {
+ public AttachmentViewHolder(@NonNull Context parentContext, @NonNull ItemAttachmentBinding binding) {
super(binding.getRoot());
+ this.parentContext = parentContext;
this.binding = binding;
}
+
+ public void bind(@NonNull Account account, @NonNull Attachment attachment, long cardRemoteId) {
+ if (MimeTypeUtil.isImage(attachment.getMimetype())) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ binding.preview.setTransitionName(parentContext.getString(R.string.transition_attachment_preview, String.valueOf(attachment.getLocalId())));
+ }
+ binding.preview.setImageResource(R.drawable.ic_image_grey600_24dp);
+ binding.preview.post(() -> {
+ final String uri = AttachmentUtil.getThumbnailUrl(account.getServerDeckVersionAsObject(), account.getUrl(), cardRemoteId, attachment, binding.preview.getWidth());
+ Glide.with(parentContext)
+ .load(uri)
+ .listener(new RequestListener<Drawable>() {
+ @Override
+ public boolean onLoadFailed(@Nullable GlideException e, Object model,
+ Target<Drawable> target, boolean isFirstResource) {
+ if (parentContext instanceof FragmentActivity) {
+ ((FragmentActivity) parentContext).supportStartPostponedEnterTransition();
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onResourceReady(Drawable resource, Object model,
+ Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
+ if (parentContext instanceof FragmentActivity) {
+ ((FragmentActivity) parentContext).supportStartPostponedEnterTransition();
+ }
+ return false;
+ }
+ })
+ .error(R.drawable.ic_image_grey600_24dp)
+ .into(binding.preview);
+ });
+ }
+ }
} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentsActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentsActivity.java
index 98cfd4440..6618f7f72 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentsActivity.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentsActivity.java
@@ -2,6 +2,7 @@ package it.niedermann.nextcloud.deck.ui.attachments;
import android.content.Context;
import android.content.Intent;
+import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.View;
@@ -9,6 +10,9 @@ import android.view.View;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.SharedElementCallback;
+import androidx.core.content.ContextCompat;
+import androidx.core.graphics.drawable.DrawableCompat;
+import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager2.widget.ViewPager2;
@@ -21,7 +25,6 @@ import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.ActivityAttachmentsBinding;
import it.niedermann.nextcloud.deck.model.Account;
import it.niedermann.nextcloud.deck.model.Attachment;
-import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler;
import it.niedermann.nextcloud.deck.util.MimeTypeUtil;
@@ -32,6 +35,7 @@ public class AttachmentsActivity extends AppCompatActivity {
private static final String BUNDLE_KEY_CURRENT_ATTACHMENT_LOCAL_ID = "currentAttachmenLocaltId";
private ActivityAttachmentsBinding binding;
+ private AttachmentsViewModel viewModel;
private ViewPager2.OnPageChangeCallback onPageChangeCallback;
@Override
@@ -40,10 +44,15 @@ public class AttachmentsActivity extends AppCompatActivity {
Thread.currentThread().setUncaughtExceptionHandler(new ExceptionHandler(this));
binding = ActivityAttachmentsBinding.inflate(getLayoutInflater());
+ viewModel = new ViewModelProvider(this).get(AttachmentsViewModel.class);
+
setContentView(binding.getRoot());
supportPostponeEnterTransition();
setSupportActionBar(binding.toolbar);
+ final Drawable navigationIcon = getResources().getDrawable(R.drawable.ic_arrow_back_white_24dp);
+ DrawableCompat.setTint(navigationIcon, ContextCompat.getColor(this, android.R.color.white));
+ binding.toolbar.setNavigationIcon(navigationIcon);
final Bundle args = getIntent().getExtras();
if (args == null || !args.containsKey(BUNDLE_KEY_ACCOUNT) || !args.containsKey(BUNDLE_KEY_CARD_ID)) {
@@ -58,8 +67,7 @@ public class AttachmentsActivity extends AppCompatActivity {
long cardId = args.getLong(BUNDLE_KEY_CARD_ID);
- final SyncManager syncManager = new SyncManager(this);
- syncManager.getCardByLocalId(account.getId(), cardId).observe(this, fullCard -> {
+ viewModel.getFullCardWithProjectsByLocalId(account.getId(), cardId).observe(this, fullCard -> {
final List<Attachment> attachments = new ArrayList<>();
for (Attachment a : fullCard.getAttachments()) {
if (MimeTypeUtil.isImage(a.getMimetype())) {
@@ -67,7 +75,7 @@ public class AttachmentsActivity extends AppCompatActivity {
}
}
if (fullCard.getAttachments().size() == 0) {
- DeckLog.logError(new IllegalStateException(AttachmentsActivity.class.getSimpleName() + " called, but card " + fullCard.getLocalId() + "has no attachments"));
+ DeckLog.logError(new IllegalStateException(AttachmentsActivity.class.getSimpleName() + " called, but card " + fullCard.getCard().getTitle() + " has no attachments"));
supportFinishAfterTransition();
return;
}
@@ -79,7 +87,7 @@ public class AttachmentsActivity extends AppCompatActivity {
binding.toolbar.setTitle(attachments.get(position).getBasename());
}
};
- RecyclerView.Adapter adapter = new AttachmentAdapter(account, fullCard.getId(), attachments);
+ RecyclerView.Adapter<AttachmentViewHolder> adapter = new AttachmentAdapter(account, fullCard.getId(), attachments);
binding.viewPager.setAdapter(adapter);
binding.viewPager.registerOnPageChangeCallback(onPageChangeCallback);
@@ -104,7 +112,7 @@ public class AttachmentsActivity extends AppCompatActivity {
long currentAttachmentLocalId = attachments.get(binding.viewPager.getCurrentItem()).getLocalId();
String transitionKey = getString(R.string.transition_attachment_preview, String.valueOf(currentAttachmentLocalId));
if (transitionKey.equals(names.get(0))) {
- sharedElements.put(transitionKey, binding.viewPager.getRootView().findViewById(R.id.preview)
+ sharedElements.put(transitionKey, binding.viewPager.getRootView().findViewById(R.id.avatar)
);
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentsViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentsViewModel.java
new file mode 100644
index 000000000..87a17470c
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentsViewModel.java
@@ -0,0 +1,25 @@
+package it.niedermann.nextcloud.deck.ui.attachments;
+
+import android.app.Application;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.LiveData;
+
+import it.niedermann.nextcloud.deck.model.full.FullCardWithProjects;
+import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
+
+@SuppressWarnings("WeakerAccess")
+public class AttachmentsViewModel extends AndroidViewModel {
+
+ private final SyncManager syncManager;
+
+ public AttachmentsViewModel(@NonNull Application application) {
+ super(application);
+ this.syncManager = new SyncManager(application);
+ }
+
+ public LiveData<FullCardWithProjects> getFullCardWithProjectsByLocalId(long accountId, long cardLocalId) {
+ return syncManager.getFullCardWithProjectsByLocalId(accountId, cardLocalId);
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/ArchiveBoardListener.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/ArchiveBoardListener.java
index b7e27aa97..ff0d3e941 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/ArchiveBoardListener.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/ArchiveBoardListener.java
@@ -4,4 +4,5 @@ import it.niedermann.nextcloud.deck.model.Board;
public interface ArchiveBoardListener {
void onArchive(Board board);
+ void onClone(Board board);
} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/BoardAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/BoardAdapter.java
index 7049cc16c..c9501ffa9 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/BoardAdapter.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/BoardAdapter.java
@@ -37,7 +37,7 @@ public class BoardAdapter extends ArrayAdapter<Board> {
TextView boardName = convertView.findViewById(R.id.boardName);
if (board != null) {
boardName.setText(board.getTitle());
- boardName.setCompoundDrawables(ViewUtil.getTintedImageView(context, R.drawable.circle_grey600_36dp, "#" + board.getColor()), null, null, null);
+ boardName.setCompoundDrawables(ViewUtil.getTintedImageView(context, R.drawable.circle_grey600_36dp, board.getColor()), null, null, null);
} else {
DeckLog.logError(new IllegalArgumentException("board at position " + position + "is null"));
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/EditBoardDialogFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/EditBoardDialogFragment.java
index e5d0a482b..9da836c4c 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/EditBoardDialogFragment.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/EditBoardDialogFragment.java
@@ -7,13 +7,13 @@ import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
+import androidx.core.content.ContextCompat;
import androidx.fragment.app.DialogFragment;
import androidx.lifecycle.ViewModelProvider;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.DialogTextColorInputBinding;
import it.niedermann.nextcloud.deck.model.full.FullBoard;
-import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
import it.niedermann.nextcloud.deck.ui.MainViewModel;
import it.niedermann.nextcloud.deck.ui.branding.BrandedAlertDialogBuilder;
import it.niedermann.nextcloud.deck.ui.branding.BrandedDialogFragment;
@@ -53,24 +53,24 @@ public class EditBoardDialogFragment extends BrandedDialogFragment {
if (args != null && args.containsKey(KEY_BOARD_ID)) {
dialogBuilder.setTitle(R.string.edit_board);
dialogBuilder.setPositiveButton(R.string.simple_save, (dialog, which) -> {
- this.fullBoard.board.setColor(binding.colorChooser.getSelectedColor().substring(1));
+ this.fullBoard.board.setColor(binding.colorChooser.getSelectedColor());
this.fullBoard.board.setTitle(binding.input.getText().toString());
- editBoardListener.onUpdateBoard(fullBoard);
+ this.editBoardListener.onUpdateBoard(fullBoard);
});
final MainViewModel viewModel = new ViewModelProvider(requireActivity()).get(MainViewModel.class);
- new SyncManager(requireActivity()).getFullBoardById(viewModel.getCurrentAccount().getId(), args.getLong(KEY_BOARD_ID)).observe(EditBoardDialogFragment.this, (FullBoard fb) -> {
+ viewModel.getFullBoardById(viewModel.getCurrentAccount().getId(), args.getLong(KEY_BOARD_ID)).observe(EditBoardDialogFragment.this, (FullBoard fb) -> {
if (fb.board != null) {
this.fullBoard = fb;
String title = this.fullBoard.getBoard().getTitle();
binding.input.setText(title);
binding.input.setSelection(title.length());
- binding.colorChooser.selectColor("#" + fullBoard.getBoard().getColor());
+ binding.colorChooser.selectColor(fullBoard.getBoard().getColor());
}
});
} else {
dialogBuilder.setTitle(R.string.add_board);
dialogBuilder.setPositiveButton(R.string.simple_add, (dialog, which) -> editBoardListener.onCreateBoard(binding.input.getText().toString(), binding.colorChooser.getSelectedColor()));
- binding.colorChooser.selectColor(String.format("#%06X", 0xFFFFFF & getResources().getColor(R.color.board_default_color)));
+ binding.colorChooser.selectColor(ContextCompat.getColor(requireContext(), R.color.board_default_color));
}
return dialogBuilder
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/EditBoardListener.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/EditBoardListener.java
index ee9ba9b9d..9d8fcdbde 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/EditBoardListener.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/EditBoardListener.java
@@ -1,11 +1,13 @@
package it.niedermann.nextcloud.deck.ui.board;
+import androidx.annotation.ColorInt;
+
import it.niedermann.nextcloud.deck.model.full.FullBoard;
public interface EditBoardListener {
void onUpdateBoard(FullBoard fullBoard);
- default void onCreateBoard(String title, String color) {
+ default void onCreateBoard(String title, @ColorInt int color) {
// Creating board is not necessary
}
} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/accesscontrol/AccessControlAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/accesscontrol/AccessControlAdapter.java
index e5a50d9f4..0a1281fae 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/accesscontrol/AccessControlAdapter.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/accesscontrol/AccessControlAdapter.java
@@ -10,6 +10,7 @@ import android.view.ViewGroup;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.SwitchCompat;
+import androidx.core.content.ContextCompat;
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.recyclerview.widget.RecyclerView;
@@ -51,7 +52,7 @@ public class AccessControlAdapter extends RecyclerView.Adapter<RecyclerView.View
this.account = account;
this.accessControlChangedListener = accessControlChangedListener;
this.context = context;
- this.mainColor = context.getResources().getColor(R.color.primary);
+ this.mainColor = ContextCompat.getColor(context, R.color.primary);
setHasStableIds(true);
}
@@ -172,9 +173,9 @@ public class AccessControlAdapter extends RecyclerView.Adapter<RecyclerView.View
final int finalMainColor = getSecondaryForegroundColorDependingOnTheme(context, mainColor);
DrawableCompat.setTintList(switchCompat.getThumbDrawable(), new ColorStateList(
new int[][]{new int[]{android.R.attr.state_checked}, new int[]{}},
- new int[]{finalMainColor, context.getResources().getColor(R.color.fg_secondary)}
+ new int[]{finalMainColor, ContextCompat.getColor(context, R.color.fg_secondary)}
));
- final int trackColor = context.getResources().getColor(R.color.fg_secondary);
+ final int trackColor = ContextCompat.getColor(context, R.color.fg_secondary);
final int lightTrackColor = Color.argb(77, Color.red(trackColor), Color.green(trackColor), Color.blue(trackColor));
final int lightTrackColorChecked = Color.argb(77, Color.red(finalMainColor), Color.green(finalMainColor), Color.blue(finalMainColor));
DrawableCompat.setTintList(switchCompat.getTrackDrawable(), new ColorStateList(
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/accesscontrol/AccessControlDialogFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/accesscontrol/AccessControlDialogFragment.java
index 33c0fe5b1..78ccd5333 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/accesscontrol/AccessControlDialogFragment.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/accesscontrol/AccessControlDialogFragment.java
@@ -43,7 +43,6 @@ public class AccessControlDialogFragment extends BrandedDialogFragment implement
private static final String KEY_BOARD_ID = "board_id";
private long boardId;
- private SyncManager syncManager;
private UserAutoCompleteAdapter userAutoCompleteAdapter;
private AccessControlAdapter adapter;
@@ -75,10 +74,9 @@ public class AccessControlDialogFragment extends BrandedDialogFragment implement
adapter = new AccessControlAdapter(viewModel.getCurrentAccount(), this, requireContext());
binding.peopleList.setAdapter(adapter);
- syncManager = new SyncManager(requireActivity());
- syncManager.getFullBoardById(viewModel.getCurrentAccount().getId(), boardId).observe(this, (FullBoard fullBoard) -> {
+ viewModel.getFullBoardById(viewModel.getCurrentAccount().getId(), boardId).observe(this, (FullBoard fullBoard) -> {
if (fullBoard != null) {
- syncManager.getAccessControlByLocalBoardId(viewModel.getCurrentAccount().getId(), boardId).observe(this, (List<AccessControl> accessControlList) -> {
+ viewModel.getAccessControlByLocalBoardId(viewModel.getCurrentAccount().getId(), boardId).observe(this, (List<AccessControl> accessControlList) -> {
final AccessControl ownerControl = new AccessControl();
ownerControl.setLocalId(HEADER_ITEM_LOCAL_ID);
ownerControl.setUser(fullBoard.getOwner());
@@ -103,15 +101,20 @@ public class AccessControlDialogFragment extends BrandedDialogFragment implement
@Override
public void updateAccessControl(AccessControl accessControl) {
- syncManager.updateAccessControl(accessControl);
+ WrappedLiveData<AccessControl> updateLiveData = viewModel.updateAccessControl(accessControl);
+ observeOnce(updateLiveData, requireActivity(), (next) -> {
+ if (updateLiveData.hasError()) {
+ ExceptionDialogFragment.newInstance(updateLiveData.getError(), viewModel.getCurrentAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ }
+ });
}
@Override
public void deleteAccessControl(AccessControl ac) {
- final WrappedLiveData<Void> wrappedDeleteLiveData = syncManager.deleteAccessControl(ac);
+ final WrappedLiveData<Void> wrappedDeleteLiveData = viewModel.deleteAccessControl(ac);
adapter.remove(ac);
observeOnce(wrappedDeleteLiveData, this, (ignored) -> {
- if (wrappedDeleteLiveData.hasError()) {
+ if (wrappedDeleteLiveData.hasError() && !SyncManager.ignoreExceptionOnVoidError(wrappedDeleteLiveData.getError())) {
DeckLog.logError(wrappedDeleteLiveData.getError());
BrandedSnackbar.make(requireView(), getString(R.string.error_revoking_ac, ac.getUser().getDisplayname()), Snackbar.LENGTH_LONG)
.setAction(R.string.simple_more, v -> ExceptionDialogFragment.newInstance(wrappedDeleteLiveData.getError(), viewModel.getCurrentAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()))
@@ -129,7 +132,12 @@ public class AccessControlDialogFragment extends BrandedDialogFragment implement
ac.setType(0L); // https://github.com/nextcloud/deck/blob/master/docs/API.md#post-boardsboardidacl---add-new-acl-rule
ac.setUserId(user.getLocalId());
ac.setUser(user);
- syncManager.createAccessControl(viewModel.getCurrentAccount().getId(), ac);
+ final WrappedLiveData<AccessControl> createLiveData = viewModel.createAccessControl(viewModel.getCurrentAccount().getId(), ac);
+ observeOnce(createLiveData, this, (next) -> {
+ if (createLiveData.hasError()) {
+ ExceptionDialogFragment.newInstance(createLiveData.getError(), viewModel.getCurrentAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ }
+ });
binding.people.setText("");
userAutoCompleteAdapter.exclude(user);
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/EditLabelDialogFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/EditLabelDialogFragment.java
index d460d1590..2dacfe6ac 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/EditLabelDialogFragment.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/EditLabelDialogFragment.java
@@ -58,14 +58,14 @@ public class EditLabelDialogFragment extends BrandedDialogFragment {
dialogBuilder.setTitle(getString(R.string.edit_tag, label.getTitle()));
dialogBuilder.setPositiveButton(R.string.simple_save, (dialog, which) -> {
- this.label.setColor(binding.colorChooser.getSelectedColor().substring(1));
+ this.label.setColor(binding.colorChooser.getSelectedColor());
this.label.setTitle(binding.input.getText().toString());
listener.onLabelUpdated(this.label);
});
String title = this.label.getTitle();
binding.input.setText(title);
binding.input.setSelection(title.length());
- binding.colorChooser.selectColor("#" + this.label.getColor());
+ binding.colorChooser.selectColor(this.label.getColor());
return dialogBuilder
.setView(binding.getRoot())
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/ManageLabelsDialogFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/ManageLabelsDialogFragment.java
index 3391c7a99..bc0d98ca3 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/ManageLabelsDialogFragment.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/ManageLabelsDialogFragment.java
@@ -38,7 +38,6 @@ public class ManageLabelsDialogFragment extends BrandedDialogFragment implements
private static final String KEY_BOARD_ID = "board_id";
private long boardId;
- private SyncManager syncManager;
@Override
public void onAttach(@NonNull Context context) {
@@ -67,8 +66,7 @@ public class ManageLabelsDialogFragment extends BrandedDialogFragment implements
colors = getResources().getStringArray(R.array.board_default_colors);
adapter = new ManageLabelsAdapter(this, requireContext());
binding.labels.setAdapter(adapter);
- syncManager = new SyncManager(requireActivity());
- syncManager.getFullBoardById(viewModel.getCurrentAccount().getId(), boardId).observe(this, (fullBoard) -> {
+ viewModel.getFullBoardById(viewModel.getCurrentAccount().getId(), boardId).observe(this, (fullBoard) -> {
if (fullBoard == null) {
throw new IllegalStateException("FullBoard should not be null");
}
@@ -80,9 +78,9 @@ public class ManageLabelsDialogFragment extends BrandedDialogFragment implements
final Label label = new Label();
label.setBoardId(boardId);
label.setTitle(binding.addLabelTitle.getText().toString());
- label.setColor(colors[new Random().nextInt(colors.length)].substring(1));
+ label.setColor(colors[new Random().nextInt(colors.length)]);
- WrappedLiveData<Label> createLiveData = syncManager.createLabel(viewModel.getCurrentAccount().getId(), label, boardId);
+ WrappedLiveData<Label> createLiveData = viewModel.createLabel(viewModel.getCurrentAccount().getId(), label, boardId);
observeOnce(createLiveData, this, (createdLabel) -> {
if (createLiveData.hasError()) {
final Throwable error = createLiveData.getError();
@@ -126,7 +124,7 @@ public class ManageLabelsDialogFragment extends BrandedDialogFragment implements
@Override
public void requestDelete(@NonNull Label label) {
- observeOnce(syncManager.countCardsWithLabel(label.getLocalId()), this, (count) -> {
+ observeOnce(viewModel.countCardsWithLabel(label.getLocalId()), this, (count) -> {
if (count > 0) {
new BrandedDeleteAlertDialogBuilder(requireContext())
.setTitle(getString(R.string.delete_something, label.getTitle()))
@@ -141,9 +139,9 @@ public class ManageLabelsDialogFragment extends BrandedDialogFragment implements
}
private void deleteLabel(@NonNull Label label) {
- final WrappedLiveData<Void> deleteLiveData = syncManager.deleteLabel(label);
+ final WrappedLiveData<Void> deleteLiveData = viewModel.deleteLabel(label);
observeOnce(deleteLiveData, this, (v) -> {
- if (deleteLiveData.hasError()) {
+ if (deleteLiveData.hasError() && !SyncManager.ignoreExceptionOnVoidError(deleteLiveData.getError())) {
final Throwable error = deleteLiveData.getError();
assert error != null;
Toast.makeText(requireContext(), error.getLocalizedMessage(), Toast.LENGTH_LONG).show();
@@ -159,7 +157,7 @@ public class ManageLabelsDialogFragment extends BrandedDialogFragment implements
@Override
public void onLabelUpdated(@NonNull Label label) {
- WrappedLiveData<Label> updateLiveData = syncManager.updateLabel(label);
+ WrappedLiveData<Label> updateLiveData = viewModel.updateLabel(label);
observeOnce(updateLiveData, this, (updatedLabel) -> {
if (updateLiveData.hasError()) {
final Throwable error = updateLiveData.getError();
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/ManageLabelsViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/ManageLabelsViewHolder.java
index 7fa3abd89..381a290e6 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/ManageLabelsViewHolder.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/ManageLabelsViewHolder.java
@@ -1,14 +1,13 @@
package it.niedermann.nextcloud.deck.ui.board.managelabels;
import android.content.res.ColorStateList;
-import android.graphics.Color;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
+import it.niedermann.android.util.ColorUtil;
import it.niedermann.nextcloud.deck.databinding.ItemManageLabelBinding;
import it.niedermann.nextcloud.deck.model.Label;
-import it.niedermann.nextcloud.deck.util.ColorUtil;
public class ManageLabelsViewHolder extends RecyclerView.ViewHolder {
private ItemManageLabelBinding binding;
@@ -17,13 +16,14 @@ public class ManageLabelsViewHolder extends RecyclerView.ViewHolder {
public ManageLabelsViewHolder(ItemManageLabelBinding binding) {
super(binding.getRoot());
this.binding = binding;
+ this.binding.label.setClickable(false);
}
public void bind(@NonNull Label label, @NonNull ManageLabelListener listener) {
binding.label.setText(label.getTitle());
- final int labelColor = Color.parseColor("#" + label.getColor());
+ final int labelColor = label.getColor();
binding.label.setChipBackgroundColor(ColorStateList.valueOf(labelColor));
- final int color = ColorUtil.getForegroundColorForBackgroundColor(labelColor);
+ final int color = ColorUtil.INSTANCE.getForegroundColorForBackgroundColor(labelColor);
binding.label.setTextColor(color);
binding.delete.setOnClickListener((v) -> listener.requestDelete(label));
binding.editText.setOnClickListener((v) -> listener.requestEdit(label));
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedAlertDialogBuilder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedAlertDialogBuilder.java
index cfeffe7dc..880e21073 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedAlertDialogBuilder.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedAlertDialogBuilder.java
@@ -9,8 +9,6 @@ import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
-import org.jetbrains.annotations.NotNull;
-
import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.getSecondaryForegroundColorDependingOnTheme;
import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.readBrandMainColor;
@@ -22,7 +20,7 @@ public class BrandedAlertDialogBuilder extends AlertDialog.Builder implements Br
super(context);
}
- @NotNull
+ @NonNull
@Override
public AlertDialog create() {
this.dialog = super.create();
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedDatePickerDialog.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedDatePickerDialog.java
index 5bef66f2c..319df7f79 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedDatePickerDialog.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedDatePickerDialog.java
@@ -17,7 +17,7 @@ import com.wdullaer.materialdatetimepicker.date.DatePickerDialog;
import java.util.Calendar;
import it.niedermann.nextcloud.deck.R;
-import it.niedermann.nextcloud.deck.util.ColorUtil;
+import it.niedermann.nextcloud.deck.util.DeckColorUtil;
import static it.niedermann.nextcloud.deck.DeckApplication.isDarkTheme;
import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.getSecondaryForegroundColorDependingOnTheme;
@@ -44,7 +44,7 @@ public class BrandedDatePickerDialog extends DatePickerDialog implements Branded
setOkColor(buttonTextColor);
setCancelColor(buttonTextColor);
// Text in picker title is always white
- setAccentColor(ColorUtil.contrastRatioIsSufficientBigAreas(Color.WHITE, mainColor) ? mainColor : ContextCompat.getColor(requireContext(), R.color.accent));
+ setAccentColor(DeckColorUtil.contrastRatioIsSufficientBigAreas(Color.WHITE, mainColor) ? mainColor : ContextCompat.getColor(requireContext(), R.color.accent));
}
/**
@@ -52,13 +52,13 @@ public class BrandedDatePickerDialog extends DatePickerDialog implements Branded
*
* @param callBack How the parent is notified that the date is set.
* @param year The initial year of the dialog.
- * @param monthOfYear The initial month of the dialog.
+ * @param monthOfYear The initial month of the dialog. [0 - 11]
* @param dayOfMonth The initial day of the dialog.
* @return a new DatePickerDialog instance.
*/
public static DatePickerDialog newInstance(OnDateSetListener callBack, int year, int monthOfYear, int dayOfMonth) {
DatePickerDialog ret = new BrandedDatePickerDialog();
- ret.initialize(callBack, year, monthOfYear, dayOfMonth);
+ ret.initialize(callBack, year, monthOfYear - 1, dayOfMonth);
return ret;
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedDeleteAlertDialogBuilder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedDeleteAlertDialogBuilder.java
index ec3cef553..d88fdd6cc 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedDeleteAlertDialogBuilder.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedDeleteAlertDialogBuilder.java
@@ -5,6 +5,7 @@ import android.content.DialogInterface;
import android.widget.Button;
import androidx.annotation.CallSuper;
+import androidx.core.content.ContextCompat;
import it.niedermann.nextcloud.deck.R;
@@ -20,7 +21,7 @@ public class BrandedDeleteAlertDialogBuilder extends BrandedAlertDialogBuilder {
super.applyBrand(mainColor);
final Button positiveButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
if (positiveButton != null) {
- positiveButton.setTextColor(getContext().getResources().getColor(R.color.danger));
+ positiveButton.setTextColor(ContextCompat.getColor(getContext(), R.color.danger));
}
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedSnackbar.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedSnackbar.java
index 20e6f8dc8..0159a59dc 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedSnackbar.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedSnackbar.java
@@ -11,8 +11,8 @@ import androidx.core.content.ContextCompat;
import com.google.android.material.snackbar.BaseTransientBottomBar;
import com.google.android.material.snackbar.Snackbar;
+import it.niedermann.android.util.ColorUtil;
import it.niedermann.nextcloud.deck.R;
-import it.niedermann.nextcloud.deck.util.ColorUtil;
import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.isBrandingEnabled;
import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.readBrandMainColor;
@@ -25,9 +25,9 @@ public class BrandedSnackbar {
final Snackbar snackbar = Snackbar.make(view, text, duration);
if (isBrandingEnabled(view.getContext())) {
@ColorInt final int color = readBrandMainColor(view.getContext());
- snackbar.setActionTextColor(ColorUtil.isColorDark(color) ? Color.WHITE : color);
+ snackbar.setActionTextColor(ColorUtil.INSTANCE.isColorDark(color) ? Color.WHITE : color);
} else {
- snackbar.setActionTextColor(ContextCompat.getColor(view.getContext(), R.color.primary));
+ snackbar.setActionTextColor(ContextCompat.getColor(view.getContext(), R.color.defaultBrand));
}
return snackbar;
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedTimePickerDialog.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedTimePickerDialog.java
index a1963aa18..2e0c4d3b8 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedTimePickerDialog.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedTimePickerDialog.java
@@ -14,10 +14,10 @@ import androidx.core.content.ContextCompat;
import com.wdullaer.materialdatetimepicker.time.TimePickerDialog;
-import java.util.Calendar;
+import java.time.LocalTime;
import it.niedermann.nextcloud.deck.R;
-import it.niedermann.nextcloud.deck.util.ColorUtil;
+import it.niedermann.nextcloud.deck.util.DeckColorUtil;
import static it.niedermann.nextcloud.deck.DeckApplication.isDarkTheme;
import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.getSecondaryForegroundColorDependingOnTheme;
@@ -44,7 +44,7 @@ public class BrandedTimePickerDialog extends TimePickerDialog implements Branded
setOkColor(buttonTextColor);
setCancelColor(buttonTextColor);
// Text in picker title is always white
- setAccentColor(ColorUtil.contrastRatioIsSufficientBigAreas(Color.WHITE, mainColor) ? mainColor : ContextCompat.getColor(requireContext(), R.color.accent));
+ setAccentColor(DeckColorUtil.contrastRatioIsSufficientBigAreas(Color.WHITE, mainColor) ? mainColor : ContextCompat.getColor(requireContext(), R.color.accent));
}
/**
@@ -86,9 +86,9 @@ public class BrandedTimePickerDialog extends TimePickerDialog implements Branded
* @param is24HourMode True to render 24 hour mode, false to render AM / PM selectors.
* @return a new TimePickerDialog instance.
*/
- @SuppressWarnings({"unused", "SameParameterValue"})
+ @SuppressWarnings({"SameParameterValue"})
public static TimePickerDialog newInstance(OnTimeSetListener callback, boolean is24HourMode) {
- Calendar now = Calendar.getInstance();
- return newInstance(callback, now.get(Calendar.HOUR_OF_DAY), now.get(Calendar.MINUTE), is24HourMode);
+ LocalTime now = LocalTime.now();
+ return newInstance(callback, now.getHour(), now.getMinute(), is24HourMode);
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandingUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandingUtil.java
index 02ad6b309..b780efec5 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandingUtil.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandingUtil.java
@@ -17,14 +17,13 @@ import androidx.preference.PreferenceManager;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.tabs.TabLayout;
+import it.niedermann.android.util.ColorUtil;
import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.R;
import static it.niedermann.nextcloud.deck.DeckApplication.isDarkTheme;
-import static it.niedermann.nextcloud.deck.util.ColorUtil.contrastRatioIsSufficient;
-import static it.niedermann.nextcloud.deck.util.ColorUtil.contrastRatioIsSufficientBigAreas;
-import static it.niedermann.nextcloud.deck.util.ColorUtil.getContrastRatio;
-import static it.niedermann.nextcloud.deck.util.ColorUtil.getForegroundColorForBackgroundColor;
+import static it.niedermann.nextcloud.deck.util.DeckColorUtil.contrastRatioIsSufficient;
+import static it.niedermann.nextcloud.deck.util.DeckColorUtil.contrastRatioIsSufficientBigAreas;
public abstract class BrandingUtil {
@@ -44,7 +43,7 @@ public abstract class BrandingUtil {
DeckLog.log("--- Read: shared_preference_theme_main");
return sharedPreferences.getInt(context.getString(R.string.shared_preference_theme_main), context.getApplicationContext().getResources().getColor(R.color.defaultBrand));
} else {
- return context.getResources().getColor(R.color.defaultBrand);
+ return ContextCompat.getColor(context, R.color.defaultBrand);
}
}
@@ -87,13 +86,13 @@ public abstract class BrandingUtil {
fab.setSupportBackgroundTintList(ColorStateList.valueOf(contrastRatioIsSufficient
? mainColor
: ContextCompat.getColor(fab.getContext(), R.color.accent)));
- fab.setColorFilter(contrastRatioIsSufficient ? getForegroundColorForBackgroundColor(mainColor) : mainColor);
+ fab.setColorFilter(contrastRatioIsSufficient ? ColorUtil.INSTANCE.getForegroundColorForBackgroundColor(mainColor) : mainColor);
}
public static void applyBrandToPrimaryTabLayout(@ColorInt int mainColor, @NonNull TabLayout tabLayout) {
- @ColorInt int finalMainColor = getSecondaryForegroundColorDependingOnTheme(tabLayout.getContext(), mainColor);
+ @ColorInt final int finalMainColor = getSecondaryForegroundColorDependingOnTheme(tabLayout.getContext(), mainColor);
tabLayout.setBackgroundColor(ContextCompat.getColor(tabLayout.getContext(), R.color.primary));
- final boolean contrastRatioIsSufficient = getContrastRatio(mainColor, ContextCompat.getColor(tabLayout.getContext(), R.color.primary)) > 1.7d;
+ final boolean contrastRatioIsSufficient = ColorUtil.INSTANCE.getContrastRatio(mainColor, ContextCompat.getColor(tabLayout.getContext(), R.color.primary)) > 1.7d;
tabLayout.setSelectedTabIndicatorColor(contrastRatioIsSufficient ? mainColor : finalMainColor);
}
@@ -112,7 +111,7 @@ public abstract class BrandingUtil {
finalMainColor,
finalMainColor,
finalMainColor,
- editText.getContext().getResources().getColor(R.color.fg_secondary)
+ ContextCompat.getColor(editText.getContext(), R.color.fg_secondary)
}
));
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/AbstractCardViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/AbstractCardViewHolder.java
new file mode 100644
index 000000000..4d3b8bb35
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/AbstractCardViewHolder.java
@@ -0,0 +1,122 @@
+package it.niedermann.nextcloud.deck.ui.card;
+
+import android.content.Context;
+import android.view.Menu;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnLongClickListener;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.CallSuper;
+import androidx.annotation.ColorInt;
+import androidx.annotation.MenuRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.widget.PopupMenu;
+import androidx.core.graphics.drawable.DrawableCompat;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.google.android.material.card.MaterialCardView;
+
+import org.jetbrains.annotations.Contract;
+
+import java.time.ZoneId;
+import java.util.List;
+
+import it.niedermann.nextcloud.deck.R;
+import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.deck.model.Card;
+import it.niedermann.nextcloud.deck.model.User;
+import it.niedermann.nextcloud.deck.model.enums.DBStatus;
+import it.niedermann.nextcloud.deck.model.full.FullCard;
+import it.niedermann.nextcloud.deck.util.DateUtil;
+import it.niedermann.nextcloud.deck.util.ViewUtil;
+
+public abstract class AbstractCardViewHolder extends RecyclerView.ViewHolder {
+
+ public AbstractCardViewHolder(@NonNull View itemView) {
+ super(itemView);
+ }
+
+ /**
+ * Removes all {@link OnClickListener} and {@link OnLongClickListener}
+ */
+ @CallSuper
+ public void bind(@NonNull FullCard fullCard, @NonNull Account account, @Nullable Long boardRemoteId, boolean hasEditPermission, @MenuRes int optionsMenu, @NonNull CardOptionsItemSelectedListener optionsItemsSelectedListener, @NonNull String counterMaxValue, @ColorInt int mainColor) {
+ final Context context = itemView.getContext();
+
+ bindCardClickListener(null);
+ bindCardLongClickListener(null);
+
+ getCardMenu().setVisibility(hasEditPermission ? View.VISIBLE : View.GONE);
+ getCardTitle().setText(fullCard.getCard().getTitle().trim());
+
+ DrawableCompat.setTint(getNotSyncedYet().getDrawable(), mainColor);
+ getNotSyncedYet().setVisibility(DBStatus.LOCAL_EDITED.equals(fullCard.getStatusEnum()) ? View.VISIBLE : View.GONE);
+
+ if (fullCard.getCard().getDueDate() != null) {
+ setupDueDate(getCardDueDate(), fullCard.getCard());
+ getCardDueDate().setVisibility(View.VISIBLE);
+ } else {
+ getCardDueDate().setVisibility(View.GONE);
+ }
+
+ getCardMenu().setOnClickListener(view -> {
+ final PopupMenu popup = new PopupMenu(context, view);
+ popup.inflate(optionsMenu);
+ final Menu menu = popup.getMenu();
+ if (containsUser(fullCard.getAssignedUsers(), account.getUserName())) {
+ menu.removeItem(menu.findItem(R.id.action_card_assign).getItemId());
+ } else {
+ menu.removeItem(menu.findItem(R.id.action_card_unassign).getItemId());
+ }
+ if (boardRemoteId == null || fullCard.getCard().getId() == null) {
+ menu.removeItem(R.id.share_link);
+ }
+
+ popup.setOnMenuItemClickListener(item -> optionsItemsSelectedListener.onCardOptionsItemSelected(item, fullCard));
+ popup.show();
+ });
+ }
+
+ protected abstract TextView getCardDueDate();
+
+ protected abstract ImageView getNotSyncedYet();
+
+ protected abstract TextView getCardTitle();
+
+ protected abstract View getCardMenu();
+
+ protected abstract MaterialCardView getCard();
+
+ public void bindCardClickListener(@Nullable OnClickListener l) {
+ getCard().setOnClickListener(l);
+ }
+
+ public void bindCardLongClickListener(@Nullable OnLongClickListener l) {
+ getCard().setOnLongClickListener(l);
+ }
+
+ public MaterialCardView getDraggable() {
+ return getCard();
+ }
+
+ private static void setupDueDate(@NonNull TextView cardDueDate, @NonNull Card card) {
+ final Context context = cardDueDate.getContext();
+ cardDueDate.setText(DateUtil.getRelativeDateTimeString(context, card.getDueDate().toEpochMilli()));
+ ViewUtil.themeDueDate(context, cardDueDate, card.getDueDate().atZone(ZoneId.systemDefault()).toLocalDate());
+ }
+
+ @Contract("null, _ -> false")
+ private static boolean containsUser(List<User> userList, String username) {
+ if (userList != null) {
+ for (User user : userList) {
+ if (user.getPrimaryKey().equals(username)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CardAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CardAdapter.java
index 986a66cad..87140e544 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CardAdapter.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CardAdapter.java
@@ -1,99 +1,82 @@
package it.niedermann.nextcloud.deck.ui.card;
-import android.annotation.SuppressLint;
import android.content.ClipData;
import android.content.Context;
import android.content.Intent;
import android.view.LayoutInflater;
-import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
-import android.widget.PopupMenu;
-import android.widget.TextView;
+import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
-import androidx.core.graphics.drawable.DrawableCompat;
+import androidx.core.content.ContextCompat;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.LifecycleOwner;
import androidx.recyclerview.widget.RecyclerView;
-import org.jetbrains.annotations.Contract;
-import org.jetbrains.annotations.NotNull;
-
import java.util.ArrayList;
-import java.util.LinkedList;
import java.util.List;
import it.niedermann.android.crosstabdnd.DragAndDropAdapter;
import it.niedermann.android.crosstabdnd.DraggedItemLocalState;
import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.R;
-import it.niedermann.nextcloud.deck.databinding.ItemCardBinding;
+import it.niedermann.nextcloud.deck.databinding.ItemCardCompactBinding;
+import it.niedermann.nextcloud.deck.databinding.ItemCardDefaultBinding;
+import it.niedermann.nextcloud.deck.databinding.ItemCardDefaultOnlyTitleBinding;
import it.niedermann.nextcloud.deck.model.Account;
import it.niedermann.nextcloud.deck.model.Card;
-import it.niedermann.nextcloud.deck.model.Label;
import it.niedermann.nextcloud.deck.model.Stack;
-import it.niedermann.nextcloud.deck.model.User;
-import it.niedermann.nextcloud.deck.model.enums.DBStatus;
import it.niedermann.nextcloud.deck.model.full.FullCard;
-import it.niedermann.nextcloud.deck.model.full.FullStack;
import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
-import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper;
import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.WrappedLiveData;
+import it.niedermann.nextcloud.deck.ui.MainViewModel;
import it.niedermann.nextcloud.deck.ui.branding.Branded;
-import it.niedermann.nextcloud.deck.ui.branding.BrandedAlertDialogBuilder;
import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment;
-import it.niedermann.nextcloud.deck.util.DateUtil;
-import it.niedermann.nextcloud.deck.util.ViewUtil;
+import it.niedermann.nextcloud.deck.ui.movecard.MoveCardDialogFragment;
+import static androidx.preference.PreferenceManager.getDefaultSharedPreferences;
import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce;
import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.getSecondaryForegroundColorDependingOnTheme;
import static it.niedermann.nextcloud.deck.util.MimeTypeUtil.TEXT_PLAIN;
-public class CardAdapter extends RecyclerView.Adapter<ItemCardViewHolder> implements DragAndDropAdapter<FullCard>, Branded {
-
- protected final SyncManager syncManager;
+public class CardAdapter extends RecyclerView.Adapter<AbstractCardViewHolder> implements DragAndDropAdapter<FullCard>, CardOptionsItemSelectedListener, Branded {
- private final FragmentManager fragmentManager;
- private final Account account;
- @Nullable
- private final Long currentBoardRemoteId;
- private final long boardId;
+ private final boolean compactMode;
+ @NonNull
+ protected final MainViewModel mainViewModel;
+ @NonNull
+ protected final FragmentManager fragmentManager;
private final long stackId;
- private final boolean canEdit;
@NonNull
private final Context context;
@Nullable
private final SelectCardListener selectCardListener;
- private List<FullCard> cardList = new LinkedList<>();
- private LifecycleOwner lifecycleOwner;
- private List<FullStack> availableStacks = new ArrayList<>();
- private String counterMaxValue;
-
- private int mainColor;
+ @NonNull
+ protected List<FullCard> cardList = new ArrayList<>();
+ @NonNull
+ protected LifecycleOwner lifecycleOwner;
+ @NonNull
+ protected String counterMaxValue;
+ @ColorInt
+ protected int mainColor;
@StringRes
- private int shareLinkRes;
+ private final int shareLinkRes;
- public CardAdapter(@NonNull Context context, @NonNull FragmentManager fragmentManager, @NonNull Account account, long boardId, @Nullable Long currentBoardRemoteId, long stackId, boolean canEdit, @NonNull SyncManager syncManager, @NonNull LifecycleOwner lifecycleOwner, @Nullable SelectCardListener selectCardListener) {
+ public CardAdapter(@NonNull Context context, @NonNull FragmentManager fragmentManager, long stackId, @NonNull MainViewModel mainViewModel, @NonNull LifecycleOwner lifecycleOwner, @Nullable SelectCardListener selectCardListener) {
this.context = context;
+ this.counterMaxValue = context.getString(R.string.counter_max_value);
this.fragmentManager = fragmentManager;
this.lifecycleOwner = lifecycleOwner;
- this.account = account;
- this.shareLinkRes = account.getServerDeckVersionAsObject().getShareLinkResource();
- this.boardId = boardId;
- this.currentBoardRemoteId = currentBoardRemoteId;
+ this.shareLinkRes = mainViewModel.getCurrentAccount().getServerDeckVersionAsObject().getShareLinkResource();
this.stackId = stackId;
- this.canEdit = canEdit;
- this.syncManager = syncManager;
+ this.mainViewModel = mainViewModel;
this.selectCardListener = selectCardListener;
- this.mainColor = context.getResources().getColor(R.color.primary);
- syncManager.getStacksForBoard(account.getId(), boardId).observe(this.lifecycleOwner, (stacks) -> {
- availableStacks.clear();
- availableStacks.addAll(stacks);
- });
+ this.mainColor = ContextCompat.getColor(context, R.color.defaultBrand);
+ this.compactMode = getDefaultSharedPreferences(context).getBoolean(context.getString(R.string.pref_key_compact), false);
setHasStableIds(true);
}
@@ -104,118 +87,62 @@ public class CardAdapter extends RecyclerView.Adapter<ItemCardViewHolder> implem
@NonNull
@Override
- public ItemCardViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int position) {
- final Context context = viewGroup.getContext();
- counterMaxValue = context.getString(R.string.counter_max_value);
+ public AbstractCardViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) {
+ if (viewType == R.layout.item_card_compact) {
+ return new CompactCardViewHolder(ItemCardCompactBinding.inflate(LayoutInflater.from(viewGroup.getContext()), viewGroup, false));
+ } else if (viewType == R.layout.item_card_default_only_title) {
+ return new DefaultCardOnlyTitleViewHolder(ItemCardDefaultOnlyTitleBinding.inflate(LayoutInflater.from(viewGroup.getContext()), viewGroup, false));
+ }
+ return new DefaultCardViewHolder(ItemCardDefaultBinding.inflate(LayoutInflater.from(viewGroup.getContext()), viewGroup, false));
+ }
- LayoutInflater layoutInflater = LayoutInflater.from(context);
- ItemCardBinding binding = ItemCardBinding.inflate(layoutInflater, viewGroup, false);
- return new ItemCardViewHolder(binding);
+ @Override
+ public int getItemViewType(int position) {
+ if (compactMode) {
+ return R.layout.item_card_compact;
+ } else {
+ final FullCard fullCard = cardList.get(position);
+ if (fullCard.getAttachments().size() == 0
+ && fullCard.getAssignedUsers().size() == 0
+ && fullCard.getLabels().size() == 0
+ && fullCard.getCommentCount() == 0) {
+ return R.layout.item_card_default_only_title;
+ }
+ return R.layout.item_card_default;
+ }
}
- @SuppressLint("SetTextI18n")
@Override
- public void onBindViewHolder(@NonNull ItemCardViewHolder viewHolder, int position) {
- final Context context = viewHolder.itemView.getContext();
- final FullCard card = cardList.get(position);
+ public void onBindViewHolder(@NonNull AbstractCardViewHolder viewHolder, int position) {
+ @NonNull FullCard fullCard = cardList.get(position);
+ viewHolder.bind(fullCard, mainViewModel.getCurrentAccount(), mainViewModel.getCurrentBoardRemoteId(), mainViewModel.currentBoardHasEditPermission(), R.menu.card_menu, this, counterMaxValue, mainColor);
- viewHolder.binding.card.setOnClickListener((v) -> {
+ // Only enable details view if there is no one waiting for selecting a card.
+ viewHolder.bindCardClickListener((v) -> {
if (selectCardListener == null) {
- context.startActivity(EditActivity.createEditCardIntent(context, account, boardId, card.getLocalId()));
+ context.startActivity(EditActivity.createEditCardIntent(context, mainViewModel.getCurrentAccount(), mainViewModel.getCurrentBoardLocalId(), fullCard.getLocalId()));
} else {
- selectCardListener.onCardSelected(card);
+ selectCardListener.onCardSelected(fullCard);
}
});
- if (canEdit && selectCardListener == null) {
- viewHolder.binding.card.setOnLongClickListener((v) -> {
+
+ // Only enable Drag and Drop if there is no one waiting for selecting a card.
+ if (selectCardListener == null) {
+ viewHolder.bindCardLongClickListener((v) -> {
DeckLog.log("Starting drag and drop");
- v.startDrag(ClipData.newPlainText("cardid", String.valueOf(card.getLocalId())),
+ v.startDrag(ClipData.newPlainText("cardid", String.valueOf(fullCard.getLocalId())),
new View.DragShadowBuilder(v),
- new DraggedItemLocalState<>(card, viewHolder.binding.card, this, position),
+ new DraggedItemLocalState<>(fullCard, viewHolder.getDraggable(), this, position),
0
);
return true;
});
- } else {
- viewHolder.binding.cardMenu.setVisibility(View.GONE);
- }
- viewHolder.binding.cardTitle.setText(card.getCard().getTitle().trim());
-
- if (card.getAssignedUsers() != null && card.getAssignedUsers().size() > 0) {
- viewHolder.binding.overlappingAvatars.setAvatars(account, card.getAssignedUsers());
- viewHolder.binding.overlappingAvatars.setVisibility(View.VISIBLE);
- } else {
- viewHolder.binding.overlappingAvatars.setVisibility(View.GONE);
}
-
- DrawableCompat.setTint(viewHolder.binding.notSyncedYet.getDrawable(), mainColor);
- viewHolder.binding.notSyncedYet.setVisibility(DBStatus.LOCAL_EDITED.equals(card.getStatusEnum()) ? View.VISIBLE : View.GONE);
-
- if (card.getCard().getDueDate() != null) {
- setupDueDate(viewHolder.binding.cardDueDate, card.getCard());
- viewHolder.binding.cardDueDate.setVisibility(View.VISIBLE);
- } else {
- viewHolder.binding.cardDueDate.setVisibility(View.GONE);
- }
-
- final int attachmentsCount = card.getAttachments().size();
-
- if (attachmentsCount == 0) {
- viewHolder.binding.cardCountAttachments.setVisibility(View.GONE);
- } else {
- setupCounter(viewHolder.binding.cardCountAttachments, attachmentsCount);
- viewHolder.binding.cardCountAttachments.setVisibility(View.VISIBLE);
- }
-
- final int commentsCount = card.getCommentCount();
-
- if (commentsCount == 0) {
- viewHolder.binding.cardCountComments.setVisibility(View.GONE);
- } else {
- setupCounter(viewHolder.binding.cardCountComments, commentsCount);
-
- viewHolder.binding.cardCountComments.setVisibility(View.VISIBLE);
- }
-
- List<Label> labels = card.getLabels();
- if (labels != null && labels.size() > 0) {
- viewHolder.binding.labels.updateLabels(labels);
- viewHolder.binding.labels.setVisibility(View.VISIBLE);
- } else {
- viewHolder.binding.labels.removeAllViews();
- viewHolder.binding.labels.setVisibility(View.GONE);
- }
-
- Card.TaskStatus taskStatus = card.getCard().getTaskStatus();
- if (taskStatus.taskCount > 0) {
- viewHolder.binding.cardCountTasks.setText(context.getResources().getString(R.string.task_count, String.valueOf(taskStatus.doneCount), String.valueOf(taskStatus.taskCount)));
- viewHolder.binding.cardCountTasks.setVisibility(View.VISIBLE);
- } else {
- viewHolder.binding.cardCountTasks.setVisibility(View.GONE);
- }
-
- viewHolder.binding.cardMenu.setOnClickListener(v -> onOverflowIconClicked(v, card));
- }
-
- private void setupCounter(@NonNull TextView textView, int count) {
- if (count > 99) {
- textView.setText(counterMaxValue);
- } else if (count > 1) {
- textView.setText(String.valueOf(count));
- } else if (count == 1) {
- textView.setText("");
- }
- }
-
- private void setupDueDate(@NonNull TextView cardDueDate, @NotNull Card card) {
- final Context context = cardDueDate.getContext();
- cardDueDate.setText(DateUtil.getRelativeDateTimeString(context, card.getDueDate().getTime()));
- ViewUtil.themeDueDate(context, cardDueDate, card.getDueDate());
}
@Override
public int getItemCount() {
- return cardList == null ? 0 : cardList.size();
+ return cardList.size();
}
public void insertItem(FullCard fullCard, int position) {
@@ -223,6 +150,7 @@ public class CardAdapter extends RecyclerView.Adapter<ItemCardViewHolder> implem
notifyItemInserted(position);
}
+ @NonNull
@Override
public List<FullCard> getItemList() {
return this.cardList;
@@ -240,114 +168,59 @@ public class CardAdapter extends RecyclerView.Adapter<ItemCardViewHolder> implem
notifyItemRemoved(position);
}
- protected void onOverflowIconClicked(@NotNull View view, FullCard card) {
- final Context context = view.getContext();
- final PopupMenu popup = new PopupMenu(context, view);
- popup.inflate(R.menu.card_menu);
- prepareOptionsMenu(popup.getMenu(), card);
-
- popup.setOnMenuItemClickListener(item -> optionsItemSelected(context, item, card));
- popup.show();
- }
-
- protected void prepareOptionsMenu(Menu menu, @NotNull FullCard card) {
- if (containsUser(card.getAssignedUsers(), account.getUserName())) {
- menu.removeItem(menu.findItem(R.id.action_card_assign).getItemId());
- } else {
- menu.removeItem(menu.findItem(R.id.action_card_unassign).getItemId());
- }
- if (currentBoardRemoteId == null || card.getCard().getId() == null) {
- menu.removeItem(R.id.share_link);
- }
- }
-
public void setCardList(@NonNull List<FullCard> cardList) {
this.cardList.clear();
this.cardList.addAll(cardList);
notifyDataSetChanged();
}
- @Contract("null, _ -> false")
- private boolean containsUser(List<User> userList, String username) {
- if (userList != null) {
- for (User user : userList) {
- if (user.getPrimaryKey().equals(username)) {
- return true;
- }
- }
- }
- return false;
+ @Override
+ public void applyBrand(int mainColor) {
+ this.mainColor = getSecondaryForegroundColorDependingOnTheme(context, mainColor);
+ notifyDataSetChanged();
}
- protected boolean optionsItemSelected(@NonNull Context context, @NotNull MenuItem item, FullCard fullCard) {
- switch (item.getItemId()) {
- case R.id.share_link: {
- Intent shareIntent = new Intent()
- .setAction(Intent.ACTION_SEND)
- .setType(TEXT_PLAIN)
- .putExtra(Intent.EXTRA_SUBJECT, fullCard.getCard().getTitle())
- .putExtra(Intent.EXTRA_TITLE, fullCard.getCard().getTitle())
- .putExtra(Intent.EXTRA_TEXT, account.getUrl() + context.getString(shareLinkRes, currentBoardRemoteId, fullCard.getCard().getId()));
- context.startActivity(Intent.createChooser(shareIntent, fullCard.getCard().getTitle()));
- }
- case R.id.action_card_assign: {
- new Thread(() -> syncManager.assignUserToCard(syncManager.getUserByUidDirectly(fullCard.getCard().getAccountId(), account.getUserName()), fullCard.getCard())).start();
- return true;
- }
- case R.id.action_card_unassign: {
- new Thread(() -> syncManager.unassignUserFromCard(syncManager.getUserByUidDirectly(fullCard.getCard().getAccountId(), account.getUserName()), fullCard.getCard())).start();
- return true;
- }
- case R.id.action_card_move: {
- int currentStackItem = 0;
- CharSequence[] items = new CharSequence[availableStacks.size()];
- for (int i = 0; i < availableStacks.size(); i++) {
- final Stack stack = availableStacks.get(i).getStack();
- items[i] = stack.getTitle();
- if (stack.getLocalId().equals(stackId)) {
- currentStackItem = i;
- }
+ @Override
+ public boolean onCardOptionsItemSelected(@NonNull MenuItem menuItem, @NonNull FullCard fullCard) {
+ int itemId = menuItem.getItemId();
+ final Account account = mainViewModel.getCurrentAccount();
+ if (itemId == R.id.share_link) {
+ Intent shareIntent = new Intent()
+ .setAction(Intent.ACTION_SEND)
+ .setType(TEXT_PLAIN)
+ .putExtra(Intent.EXTRA_SUBJECT, fullCard.getCard().getTitle())
+ .putExtra(Intent.EXTRA_TITLE, fullCard.getCard().getTitle())
+ .putExtra(Intent.EXTRA_TEXT, account.getUrl() + context.getString(shareLinkRes, mainViewModel.getCurrentBoardRemoteId(), fullCard.getCard().getId()));
+ context.startActivity(Intent.createChooser(shareIntent, fullCard.getCard().getTitle()));
+ new Thread(() -> mainViewModel.assignUserToCard(mainViewModel.getUserByUidDirectly(fullCard.getCard().getAccountId(), account.getUserName()), fullCard.getCard())).start();
+ return true;
+ } else if (itemId == R.id.action_card_assign) {
+ new Thread(() -> mainViewModel.assignUserToCard(mainViewModel.getUserByUidDirectly(fullCard.getCard().getAccountId(), account.getUserName()), fullCard.getCard())).start();
+ return true;
+ } else if (itemId == R.id.action_card_unassign) {
+ new Thread(() -> mainViewModel.unassignUserFromCard(mainViewModel.getUserByUidDirectly(fullCard.getCard().getAccountId(), account.getUserName()), fullCard.getCard())).start();
+ return true;
+ } else if (itemId == R.id.action_card_move) {
+ DeckLog.verbose("[Move card] Launch move dialog for " + Card.class.getSimpleName() + " \"" + fullCard.getCard().getTitle() + "\" (#" + fullCard.getLocalId() + ") from " + Stack.class.getSimpleName() + " #" + +stackId);
+ MoveCardDialogFragment.newInstance(fullCard.getAccountId(), mainViewModel.getCurrentBoardLocalId(), fullCard.getCard().getTitle(), fullCard.getLocalId()).show(fragmentManager, MoveCardDialogFragment.class.getSimpleName());
+ return true;
+ } else if (itemId == R.id.action_card_archive) {
+ final WrappedLiveData<FullCard> archiveLiveData = mainViewModel.archiveCard(fullCard);
+ observeOnce(archiveLiveData, lifecycleOwner, (v) -> {
+ if (archiveLiveData.hasError()) {
+ ExceptionDialogFragment.newInstance(archiveLiveData.getError(), account).show(fragmentManager, ExceptionDialogFragment.class.getSimpleName());
}
- final FullCard newCard = fullCard;
- new BrandedAlertDialogBuilder(context)
- .setSingleChoiceItems(items, currentStackItem, (dialog, which) -> {
- dialog.cancel();
- newCard.getCard().setStackId(availableStacks.get(which).getStack().getLocalId());
- LiveDataHelper.observeOnce(syncManager.updateCard(newCard), lifecycleOwner, (c) -> {
- // Nothing to do here...
- });
- DeckLog.log("Moved card \"" + fullCard.getCard().getTitle() + "\" to \"" + availableStacks.get(which).getStack().getTitle() + "\"");
- })
- .setNeutralButton(android.R.string.cancel, null)
- .setTitle(context.getString(R.string.action_card_move_title, fullCard.getCard().getTitle()))
- .show();
- return true;
- }
- case R.id.action_card_archive: {
- final WrappedLiveData<FullCard> archiveLiveData = syncManager.archiveCard(fullCard);
- observeOnce(archiveLiveData, lifecycleOwner, (v) -> {
- if (archiveLiveData.hasError()) {
- ExceptionDialogFragment.newInstance(archiveLiveData.getError(), account).show(fragmentManager, ExceptionDialogFragment.class.getSimpleName());
- }
- });
- return true;
- }
- case R.id.action_card_delete: {
- final WrappedLiveData<Void> deleteLiveData = syncManager.deleteCard(fullCard.getCard());
- observeOnce(deleteLiveData, lifecycleOwner, (v) -> {
- if (deleteLiveData.hasError()) {
- ExceptionDialogFragment.newInstance(deleteLiveData.getError(), account).show(fragmentManager, ExceptionDialogFragment.class.getSimpleName());
- }
- });
- return true;
- }
+ });
+ return true;
+ } else if (itemId == R.id.action_card_delete) {
+ final WrappedLiveData<Void> deleteLiveData = mainViewModel.deleteCard(fullCard.getCard());
+ observeOnce(deleteLiveData, lifecycleOwner, (v) -> {
+ if (deleteLiveData.hasError() && !SyncManager.ignoreExceptionOnVoidError(deleteLiveData.getError())) {
+ ExceptionDialogFragment.newInstance(deleteLiveData.getError(), account).show(fragmentManager, ExceptionDialogFragment.class.getSimpleName());
+ }
+ });
+ return true;
}
return true;
}
-
- @Override
- public void applyBrand(int mainColor) {
- this.mainColor = getSecondaryForegroundColorDependingOnTheme(context, mainColor);
- notifyDataSetChanged();
- }
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CardOptionsItemSelectedListener.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CardOptionsItemSelectedListener.java
new file mode 100644
index 000000000..d3050b732
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CardOptionsItemSelectedListener.java
@@ -0,0 +1,11 @@
+package it.niedermann.nextcloud.deck.ui.card;
+
+import android.view.MenuItem;
+
+import androidx.annotation.NonNull;
+
+import it.niedermann.nextcloud.deck.model.full.FullCard;
+
+public interface CardOptionsItemSelectedListener {
+ boolean onCardOptionsItemSelected(@NonNull MenuItem menuItem, @NonNull FullCard fullCard);
+} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CompactCardViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CompactCardViewHolder.java
new file mode 100644
index 000000000..e9f366d99
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CompactCardViewHolder.java
@@ -0,0 +1,84 @@
+package it.niedermann.nextcloud.deck.ui.card;
+
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnLongClickListener;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.MenuRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.google.android.material.card.MaterialCardView;
+
+import java.util.List;
+
+import it.niedermann.nextcloud.deck.databinding.ItemCardCompactBinding;
+import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.deck.model.Label;
+import it.niedermann.nextcloud.deck.model.full.FullCard;
+
+public class CompactCardViewHolder extends AbstractCardViewHolder {
+ private ItemCardCompactBinding binding;
+
+ @SuppressWarnings("WeakerAccess")
+ public CompactCardViewHolder(@NonNull ItemCardCompactBinding binding) {
+ super(binding.getRoot());
+ this.binding = binding;
+ }
+
+ /**
+ * Removes all {@link OnClickListener} and {@link OnLongClickListener}
+ */
+ public void bind(@NonNull FullCard fullCard, @NonNull Account account, @Nullable Long boardRemoteId, boolean hasEditPermission, @MenuRes int optionsMenu, @NonNull CardOptionsItemSelectedListener optionsItemsSelectedListener, @NonNull String counterMaxValue, @ColorInt int mainColor) {
+ super.bind(fullCard, account, boardRemoteId, hasEditPermission, optionsMenu, optionsItemsSelectedListener, counterMaxValue, mainColor);
+
+ List<Label> labels = fullCard.getLabels();
+ if (labels != null && labels.size() > 0) {
+ binding.labels.updateLabels(labels);
+ binding.labels.setVisibility(View.VISIBLE);
+ } else {
+ binding.labels.removeAllViews();
+ binding.labels.setVisibility(View.GONE);
+ }
+ }
+
+ public void bindCardClickListener(@Nullable OnClickListener l) {
+ binding.card.setOnClickListener(l);
+ }
+
+ public void bindCardLongClickListener(@Nullable OnLongClickListener l) {
+ binding.card.setOnLongClickListener(l);
+ }
+
+ public MaterialCardView getDraggable() {
+ return binding.card;
+ }
+
+ @Override
+ protected TextView getCardDueDate() {
+ return binding.cardDueDate;
+ }
+
+ @Override
+ protected ImageView getNotSyncedYet() {
+ return binding.notSyncedYet;
+ }
+
+ @Override
+ protected TextView getCardTitle() {
+ return binding.cardTitle;
+ }
+
+ @Override
+ protected View getCardMenu() {
+ return binding.cardMenu;
+ }
+
+ @Override
+ protected MaterialCardView getCard() {
+ return binding.card;
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/DefaultCardOnlyTitleViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/DefaultCardOnlyTitleViewHolder.java
new file mode 100644
index 000000000..2f9e132c9
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/DefaultCardOnlyTitleViewHolder.java
@@ -0,0 +1,61 @@
+package it.niedermann.nextcloud.deck.ui.card;
+
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnLongClickListener;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.google.android.material.card.MaterialCardView;
+
+import it.niedermann.nextcloud.deck.databinding.ItemCardDefaultOnlyTitleBinding;
+
+public class DefaultCardOnlyTitleViewHolder extends AbstractCardViewHolder {
+ private ItemCardDefaultOnlyTitleBinding binding;
+
+ @SuppressWarnings("WeakerAccess")
+ public DefaultCardOnlyTitleViewHolder(@NonNull ItemCardDefaultOnlyTitleBinding binding) {
+ super(binding.getRoot());
+ this.binding = binding;
+ }
+
+ public void bindCardClickListener(@Nullable OnClickListener l) {
+ binding.card.setOnClickListener(l);
+ }
+
+ public void bindCardLongClickListener(@Nullable OnLongClickListener l) {
+ binding.card.setOnLongClickListener(l);
+ }
+
+ public MaterialCardView getDraggable() {
+ return binding.card;
+ }
+
+ @Override
+ protected TextView getCardDueDate() {
+ return binding.cardDueDate;
+ }
+
+ @Override
+ protected ImageView getNotSyncedYet() {
+ return binding.notSyncedYet;
+ }
+
+ @Override
+ protected TextView getCardTitle() {
+ return binding.cardTitle;
+ }
+
+ @Override
+ protected View getCardMenu() {
+ return binding.cardMenu;
+ }
+
+ @Override
+ protected MaterialCardView getCard() {
+ return binding.card;
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/DefaultCardViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/DefaultCardViewHolder.java
new file mode 100644
index 000000000..6025cdada
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/DefaultCardViewHolder.java
@@ -0,0 +1,157 @@
+package it.niedermann.nextcloud.deck.ui.card;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnLongClickListener;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.MenuRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
+
+import com.google.android.material.card.MaterialCardView;
+
+import org.jetbrains.annotations.Contract;
+
+import java.util.List;
+
+import it.niedermann.nextcloud.deck.R;
+import it.niedermann.nextcloud.deck.databinding.ItemCardDefaultBinding;
+import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.deck.model.Card.TaskStatus;
+import it.niedermann.nextcloud.deck.model.Label;
+import it.niedermann.nextcloud.deck.model.User;
+import it.niedermann.nextcloud.deck.model.full.FullCard;
+
+public class DefaultCardViewHolder extends AbstractCardViewHolder {
+ private ItemCardDefaultBinding binding;
+
+ @SuppressWarnings("WeakerAccess")
+ public DefaultCardViewHolder(@NonNull ItemCardDefaultBinding binding) {
+ super(binding.getRoot());
+ this.binding = binding;
+ }
+
+ /**
+ * Removes all {@link OnClickListener} and {@link OnLongClickListener}
+ */
+ public void bind(@NonNull FullCard fullCard, @NonNull Account account, @Nullable Long boardRemoteId, boolean hasEditPermission, @MenuRes int optionsMenu, @NonNull CardOptionsItemSelectedListener optionsItemsSelectedListener, @NonNull String counterMaxValue, @ColorInt int mainColor) {
+ super.bind(fullCard, account, boardRemoteId, hasEditPermission, optionsMenu, optionsItemsSelectedListener, counterMaxValue, mainColor);
+
+ final Context context = itemView.getContext();
+
+ if (fullCard.getAssignedUsers() != null && fullCard.getAssignedUsers().size() > 0) {
+ binding.overlappingAvatars.setAvatars(account, fullCard.getAssignedUsers());
+ binding.overlappingAvatars.setVisibility(View.VISIBLE);
+ } else {
+ binding.overlappingAvatars.setVisibility(View.GONE);
+ }
+
+ final int attachmentsCount = fullCard.getAttachments().size();
+ if (attachmentsCount == 0) {
+ binding.cardCountAttachments.setVisibility(View.GONE);
+ } else {
+ setupCounter(binding.cardCountAttachments, counterMaxValue, attachmentsCount);
+ binding.cardCountAttachments.setVisibility(View.VISIBLE);
+ }
+
+ final int commentsCount = fullCard.getCommentCount();
+ if (commentsCount == 0) {
+ binding.cardCountComments.setVisibility(View.GONE);
+ } else {
+ setupCounter(binding.cardCountComments, counterMaxValue, commentsCount);
+
+ binding.cardCountComments.setVisibility(View.VISIBLE);
+ }
+
+ final List<Label> labels = fullCard.getLabels();
+ if (labels != null && labels.size() > 0) {
+ binding.labels.updateLabels(labels);
+ binding.labels.setVisibility(View.VISIBLE);
+ } else {
+ binding.labels.removeAllViews();
+ binding.labels.setVisibility(View.GONE);
+ }
+
+ final TaskStatus taskStatus = fullCard.getCard().getTaskStatus();
+ if (taskStatus.taskCount > 0) {
+ binding.cardCountTasks.setText(context.getResources().getString(R.string.task_count, String.valueOf(taskStatus.doneCount), String.valueOf(taskStatus.taskCount)));
+ binding.cardCountTasks.setCompoundDrawablesWithIntrinsicBounds(ContextCompat.getDrawable(context, R.drawable.ic_check_grey600_24dp), null, null, null);
+ binding.cardCountTasks.setVisibility(View.VISIBLE);
+ } else {
+ final String description = fullCard.getCard().getDescription();
+ if (!TextUtils.isEmpty(description)) {
+ binding.cardCountTasks.setCompoundDrawablesWithIntrinsicBounds(ContextCompat.getDrawable(context, R.drawable.ic_baseline_subject_24), null, null, null);
+ binding.cardCountTasks.setText(null);
+ binding.cardCountTasks.setVisibility(View.VISIBLE);
+ } else {
+ binding.cardCountTasks.setVisibility(View.GONE);
+ }
+ }
+ }
+
+ @Override
+ protected TextView getCardDueDate() {
+ return binding.cardDueDate;
+ }
+
+ @Override
+ protected ImageView getNotSyncedYet() {
+ return binding.notSyncedYet;
+ }
+
+ @Override
+ protected TextView getCardTitle() {
+ return binding.cardTitle;
+ }
+
+ @Override
+ protected View getCardMenu() {
+ return binding.cardMenu;
+ }
+
+ @Override
+ protected MaterialCardView getCard() {
+ return binding.card;
+ }
+
+ public void bindCardClickListener(@Nullable OnClickListener l) {
+ binding.card.setOnClickListener(l);
+ }
+
+ public void bindCardLongClickListener(@Nullable OnLongClickListener l) {
+ binding.card.setOnLongClickListener(l);
+ }
+
+ public MaterialCardView getDraggable() {
+ return binding.card;
+ }
+
+
+ private static void setupCounter(@NonNull TextView textView, @NonNull String counterMaxValue, int count) {
+ if (count > 99) {
+ textView.setText(counterMaxValue);
+ } else if (count > 1) {
+ textView.setText(String.valueOf(count));
+ } else if (count == 1) {
+ textView.setText("");
+ }
+ }
+
+ @Contract("null, _ -> false")
+ private static boolean containsUser(List<User> userList, String username) {
+ if (userList != null) {
+ for (User user : userList) {
+ if (user.getPrimaryKey().equals(username)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+} \ No newline at end of file
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 0710b7d7c..2e1ff5e06 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,13 +25,12 @@ 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.persistence.sync.SyncManager;
+import it.niedermann.nextcloud.deck.model.full.FullCard;
import it.niedermann.nextcloud.deck.ui.branding.BrandedActivity;
import it.niedermann.nextcloud.deck.ui.branding.BrandedAlertDialogBuilder;
import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler;
import it.niedermann.nextcloud.deck.util.CardUtil;
-import static android.graphics.Color.parseColor;
import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce;
import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.applyBrandToPrimaryTabLayout;
import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.isBrandingEnabled;
@@ -45,7 +44,6 @@ public class EditActivity extends BrandedActivity {
private ActivityEditBinding binding;
private EditCardViewModel viewModel;
- private SyncManager syncManager;
private static final int[] tabTitles = new int[]{
R.string.card_edit_details,
@@ -83,7 +81,6 @@ public class EditActivity extends BrandedActivity {
setSupportActionBar(binding.toolbar);
viewModel = new ViewModelProvider(this).get(EditCardViewModel.class);
- syncManager = new SyncManager(this);
loadDataFromIntent();
}
@@ -120,8 +117,8 @@ public class EditActivity extends BrandedActivity {
final long boardId = args.getLong(BUNDLE_KEY_BOARD_ID);
- observeOnce(syncManager.getFullBoardById(account.getId(), boardId), EditActivity.this, (fullBoard -> {
- applyBrand(parseColor('#' + fullBoard.getBoard().getColor()));
+ observeOnce(viewModel.getFullBoardById(account.getId(), boardId), EditActivity.this, (fullBoard -> {
+ applyBrand(fullBoard.getBoard().getColor());
viewModel.setCanEdit(fullBoard.getBoard().isPermissionEdit());
invalidateOptionsMenu();
if (viewModel.isCreateMode()) {
@@ -138,7 +135,7 @@ public class EditActivity extends BrandedActivity {
setupViewPager();
setupTitle();
} else {
- observeOnce(syncManager.getCardByLocalId(account.getId(), cardId), EditActivity.this, (fullCard) -> {
+ observeOnce(viewModel.getFullCardWithProjectsByLocalId(account.getId(), cardId), EditActivity.this, (fullCard) -> {
if (fullCard == null) {
new BrandedAlertDialogBuilder(this)
.setTitle(R.string.card_not_found)
@@ -154,6 +151,8 @@ public class EditActivity extends BrandedActivity {
});
}
}));
+
+ DeckLog.verbose("Finished loading intent data: { accountId = " + viewModel.getAccount().getId() + " , cardId = " + cardId + " }");
}
@Override
@@ -170,12 +169,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();
@@ -193,9 +196,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(viewModel.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(viewModel.updateCard(viewModel.getFullCard()), EditActivity.this, (card) -> runnable.run());
}
}
}
@@ -264,26 +267,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();
}
}
@@ -296,10 +288,10 @@ public class EditActivity extends BrandedActivity {
@Override
public void applyBrand(int mainColor) {
- if(isBrandingEnabled(this)) {
+ if (isBrandingEnabled(this)) {
final Drawable navigationIcon = binding.toolbar.getNavigationIcon();
if (navigationIcon == null) {
- DeckLog.error("Excpected navigationIcon to be present.");
+ DeckLog.error("Expected navigationIcon to be present.");
} else {
DrawableCompat.setTint(binding.toolbar.getNavigationIcon(), colorAccent);
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditCardViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditCardViewModel.java
index c8c93c838..c754d0800 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditCardViewModel.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditCardViewModel.java
@@ -1,38 +1,57 @@
package it.niedermann.nextcloud.deck.ui.card;
+import android.app.Application;
+
import androidx.annotation.NonNull;
-import androidx.lifecycle.ViewModel;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.LiveData;
+import java.io.File;
import java.util.ArrayList;
+import java.util.List;
import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.deck.model.Attachment;
+import it.niedermann.nextcloud.deck.model.Board;
import it.niedermann.nextcloud.deck.model.Card;
+import it.niedermann.nextcloud.deck.model.Label;
+import it.niedermann.nextcloud.deck.model.full.FullBoard;
import it.niedermann.nextcloud.deck.model.full.FullCard;
+import it.niedermann.nextcloud.deck.model.full.FullCardWithProjects;
+import it.niedermann.nextcloud.deck.model.ocs.Activity;
+import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
+import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.WrappedLiveData;
@SuppressWarnings("WeakerAccess")
-public class EditCardViewModel extends ViewModel {
+public class EditCardViewModel extends AndroidViewModel {
+ private SyncManager syncManager;
private Account account;
private long boardId;
- private FullCard originalCard;
- private FullCard fullCard;
+ private FullCardWithProjects originalCard;
+ private FullCardWithProjects fullCard;
private boolean isSupportedVersion = false;
private boolean hasCommentsAbility = false;
private boolean pendingCreation = false;
private boolean canEdit = false;
private boolean createMode = false;
+ public EditCardViewModel(@NonNull Application application) {
+ super(application);
+ this.syncManager = new SyncManager(application);
+ }
+
/**
* Stores a deep copy of the given fullCard to be able to compare the state at every time in #{@link EditCardViewModel#hasChanges()}
*
* @param boardId Local ID, expecting a positive long value
* @param fullCard The card that is currently edited
*/
- public void initializeExistingCard(long boardId, @NonNull FullCard fullCard, boolean isSupportedVersion) {
+ public void initializeExistingCard(long boardId, @NonNull FullCardWithProjects fullCard, boolean isSupportedVersion) {
this.boardId = boardId;
this.fullCard = fullCard;
- this.originalCard = new FullCard(this.fullCard);
+ this.originalCard = new FullCardWithProjects(this.fullCard);
this.isSupportedVersion = isSupportedVersion;
}
@@ -43,7 +62,7 @@ public class EditCardViewModel extends ViewModel {
* @param stackId Local ID, expecting a positive long value where the card should be created
*/
public void initializeNewCard(long boardId, long stackId, boolean isSupportedVersion) {
- final FullCard fullCard = new FullCard();
+ final FullCardWithProjects fullCard = new FullCardWithProjects();
fullCard.setLabels(new ArrayList<>());
fullCard.setAssignedUsers(new ArrayList<>());
fullCard.setAttachments(new ArrayList<>());
@@ -55,11 +74,12 @@ public class EditCardViewModel extends ViewModel {
public void setAccount(@NonNull Account account) {
this.account = account;
+ this.syncManager = new SyncManager(getApplication(), account.getName());
hasCommentsAbility = account.getServerDeckVersionAsObject().supportsComments();
}
public boolean hasChanges() {
- if(fullCard == null) {
+ if (fullCard == null) {
DeckLog.info("Can not check for changes because fullCard is null → assuming no changes have been made yet.");
return false;
}
@@ -74,7 +94,7 @@ public class EditCardViewModel extends ViewModel {
return account;
}
- public FullCard getFullCard() {
+ public FullCardWithProjects getFullCard() {
return fullCard;
}
@@ -105,4 +125,44 @@ public class EditCardViewModel extends ViewModel {
public long getBoardId() {
return boardId;
}
+
+ public LiveData<FullBoard> getFullBoardById(Long accountId, Long localId) {
+ return syncManager.getFullBoardById(accountId, localId);
+ }
+
+ public WrappedLiveData<Label> createLabel(long accountId, Label label, long localBoardId) {
+ return syncManager.createLabel(accountId, label, localBoardId);
+ }
+
+ public LiveData<FullCardWithProjects> getFullCardWithProjectsByLocalId(long accountId, long cardLocalId) {
+ return syncManager.getFullCardWithProjectsByLocalId(accountId, cardLocalId);
+ }
+
+ public WrappedLiveData<FullCard> createFullCard(long accountId, long localBoardId, long localStackId, @NonNull FullCard card) {
+ return syncManager.createFullCard(accountId, localBoardId, localStackId, card);
+ }
+
+ public WrappedLiveData<FullCard> updateCard(@NonNull FullCard card) {
+ return syncManager.updateCard(card);
+ }
+
+ public LiveData<List<Activity>> syncActivitiesForCard(@NonNull Card card) {
+ return syncManager.syncActivitiesForCard(card);
+ }
+
+ public WrappedLiveData<Attachment> addAttachmentToCard(long accountId, long localCardId, @NonNull String mimeType, @NonNull File file) {
+ return syncManager.addAttachmentToCard(accountId, localCardId, mimeType, file);
+ }
+
+ public WrappedLiveData<Void> deleteAttachmentOfCard(long accountId, long localCardId, long localAttachmentId) {
+ return syncManager.deleteAttachmentOfCard(accountId, localCardId, localAttachmentId);
+ }
+
+ public LiveData<Card> getCardByRemoteID(long accountId, long remoteId) {
+ return syncManager.getCardByRemoteID(accountId, remoteId);
+ }
+
+ public LiveData<Board> getBoardByRemoteId(long accountId, long remoteId) {
+ return syncManager.getBoardByRemoteId(accountId, remoteId);
+ }
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/ItemCardViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/ItemCardViewHolder.java
deleted file mode 100644
index c9e41c37f..000000000
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/ItemCardViewHolder.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package it.niedermann.nextcloud.deck.ui.card;
-
-import androidx.recyclerview.widget.RecyclerView;
-
-import it.niedermann.nextcloud.deck.databinding.ItemCardBinding;
-
-public class ItemCardViewHolder extends RecyclerView.ViewHolder {
- public ItemCardBinding binding;
-
- @SuppressWarnings("WeakerAccess")
- public ItemCardViewHolder(ItemCardBinding binding) {
- super(binding.getRoot());
- this.binding = binding;
- }
-} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/LabelAutoCompleteAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/LabelAutoCompleteAdapter.java
index f77282e11..fe974f2a0 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/LabelAutoCompleteAdapter.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/LabelAutoCompleteAdapter.java
@@ -9,6 +9,7 @@ import android.view.ViewGroup;
import android.widget.Filter;
import androidx.activity.ComponentActivity;
+import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.graphics.drawable.DrawableCompat;
@@ -18,11 +19,11 @@ import java.util.Collection;
import java.util.List;
import java.util.Random;
+import it.niedermann.android.util.ColorUtil;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.ItemAutocompleteLabelBinding;
import it.niedermann.nextcloud.deck.model.Label;
import it.niedermann.nextcloud.deck.util.AutoCompleteAdapter;
-import it.niedermann.nextcloud.deck.util.ColorUtil;
import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce;
@@ -35,7 +36,7 @@ public class LabelAutoCompleteAdapter extends AutoCompleteAdapter<Label> {
public LabelAutoCompleteAdapter(@NonNull ComponentActivity activity, long accountId, long boardId, long cardId) {
super(activity, accountId, boardId, cardId);
final String[] colors = activity.getResources().getStringArray(R.array.board_default_colors);
- final String createLabelColor = colors[new Random().nextInt(colors.length)].substring(1);
+ @ColorInt int createLabelColor = Color.parseColor(colors[new Random().nextInt(colors.length)]);
observeOnce(syncManager.getFullBoardById(accountId, boardId), activity, (fullBoard) -> {
if (fullBoard.getBoard().isPermissionManage()) {
canManage = true;
@@ -59,8 +60,8 @@ public class LabelAutoCompleteAdapter extends AutoCompleteAdapter<Label> {
}
final Label label = getItem(position);
- final int labelColor = Color.parseColor("#" + label.getColor());
- final int color = ColorUtil.getForegroundColorForBackgroundColor(labelColor);
+ final int labelColor = label.getColor();
+ final int color = ColorUtil.INSTANCE.getForegroundColorForBackgroundColor(labelColor);
binding.label.setText(label.getTitle());
binding.label.setChipBackgroundColor(ColorStateList.valueOf(labelColor));
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/UserAutoCompleteAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/UserAutoCompleteAdapter.java
index d2fd4d40d..473ad06c1 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/UserAutoCompleteAdapter.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/UserAutoCompleteAdapter.java
@@ -8,6 +8,7 @@ import android.widget.Filter;
import androidx.activity.ComponentActivity;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
+import androidx.lifecycle.Observer;
import java.util.List;
@@ -15,14 +16,16 @@ import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.ItemAutocompleteUserBinding;
import it.niedermann.nextcloud.deck.model.Account;
import it.niedermann.nextcloud.deck.model.User;
+import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.extrawurst.UserSearchLiveData;
import it.niedermann.nextcloud.deck.util.AutoCompleteAdapter;
import it.niedermann.nextcloud.deck.util.ViewUtil;
-import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce;
-
public class UserAutoCompleteAdapter extends AutoCompleteAdapter<User> {
@NonNull
private Account account;
+ private UserSearchLiveData liveSearchForACL;
+ private LiveData<List<User>> liveData;
+ private Observer<List<User>> observer;
public UserAutoCompleteAdapter(@NonNull ComponentActivity activity, @NonNull Account account, long boardId) {
this(activity, account, boardId, NO_CARD);
@@ -31,6 +34,7 @@ public class UserAutoCompleteAdapter extends AutoCompleteAdapter<User> {
public UserAutoCompleteAdapter(@NonNull ComponentActivity activity, @NonNull Account account, long boardId, long cardId) {
super(activity, account.getId(), boardId, cardId);
this.account = account;
+ this.liveSearchForACL = syncManager.searchUserByUidOrDisplayNameForACL();
}
@Override
@@ -56,23 +60,24 @@ public class UserAutoCompleteAdapter extends AutoCompleteAdapter<User> {
protected FilterResults performFiltering(CharSequence constraint) {
if (constraint != null) {
activity.runOnUiThread(() -> {
- LiveData<List<User>> liveData;
final int constraintLength = constraint.toString().trim().length();
if (cardId == NO_CARD) {
liveData = constraintLength > 0
- ? syncManager.searchUserByUidOrDisplayNameForACL(accountId, boardId, constraint.toString())
+ ? liveSearchForACL.search(accountId, boardId, constraint.toString())
: syncManager.findProposalsForUsersToAssignForACL(accountId, boardId, activity.getResources().getInteger(R.integer.max_users_suggested));
} else {
liveData = constraintLength > 0
? syncManager.searchUserByUidOrDisplayName(accountId, boardId, cardId, constraint.toString())
: syncManager.findProposalsForUsersToAssign(accountId, boardId, cardId, activity.getResources().getInteger(R.integer.max_users_suggested));
}
- observeOnce(liveData, activity, users -> {
+ liveData.removeObservers(activity);
+ observer = users -> {
users.removeAll(itemsToExclude);
filterResults.values = users;
filterResults.count = users.size();
publishResults(constraint, filterResults);
- });
+ };
+ liveData.observe(activity, observer);
});
}
return filterResults;
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/activities/CardActivityFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/activities/CardActivityFragment.java
index 08d960257..f95eea89f 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/activities/CardActivityFragment.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/activities/CardActivityFragment.java
@@ -8,11 +8,9 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
-import androidx.recyclerview.widget.RecyclerView;
import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.databinding.FragmentCardEditTabActivitiesBinding;
-import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
import it.niedermann.nextcloud.deck.ui.card.EditCardViewModel;
public class CardActivityFragment extends Fragment {
@@ -39,17 +37,14 @@ public class CardActivityFragment extends Fragment {
}
if (!viewModel.isCreateMode()) {
- final SyncManager syncManager = new SyncManager(requireContext());
-
- syncManager.syncActivitiesForCard(viewModel.getFullCard().getCard()).observe(getViewLifecycleOwner(), (activities -> {
+ viewModel.syncActivitiesForCard(viewModel.getFullCard().getCard()).observe(getViewLifecycleOwner(), (activities -> {
if (activities == null || activities.size() == 0) {
binding.emptyContentView.setVisibility(View.VISIBLE);
binding.activitiesList.setVisibility(View.GONE);
} else {
binding.emptyContentView.setVisibility(View.GONE);
binding.activitiesList.setVisibility(View.VISIBLE);
- RecyclerView.Adapter adapter = new CardActivityAdapter(activities, requireActivity().getMenuInflater());
- binding.activitiesList.setAdapter(adapter);
+ binding.activitiesList.setAdapter(new CardActivityAdapter(activities, requireActivity().getMenuInflater()));
}
}));
} else {
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/activities/CardActivityViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/activities/CardActivityViewHolder.java
index 7d49b932d..6362f90dd 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/activities/CardActivityViewHolder.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/activities/CardActivityViewHolder.java
@@ -3,17 +3,18 @@ package it.niedermann.nextcloud.deck.ui.card.activities;
import android.content.Context;
import android.view.MenuInflater;
import android.view.View;
+import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
+import it.niedermann.android.util.ClipboardUtil;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.ItemActivityBinding;
import it.niedermann.nextcloud.deck.model.enums.ActivityType;
import it.niedermann.nextcloud.deck.model.ocs.Activity;
import it.niedermann.nextcloud.deck.util.DateUtil;
-
-import static it.niedermann.nextcloud.deck.util.ClipboardUtil.copyToClipboard;
+import it.niedermann.nextcloud.deck.util.ViewUtil;
public class CardActivityViewHolder extends RecyclerView.ViewHolder {
public ItemActivityBinding binding;
@@ -26,38 +27,61 @@ public class CardActivityViewHolder extends RecyclerView.ViewHolder {
public void bind(@NonNull Activity activity, @NonNull MenuInflater inflater) {
final Context context = itemView.getContext();
- binding.date.setText(DateUtil.getRelativeDateTimeString(context, activity.getLastModified().getTime()));
+ binding.date.setText(DateUtil.getRelativeDateTimeString(context, activity.getLastModified().toEpochMilli()));
binding.subject.setText(activity.getSubject());
itemView.setOnClickListener(View::showContextMenu);
itemView.setOnCreateContextMenuListener((menu, v, menuInfo) -> {
inflater.inflate(R.menu.activity_menu, menu);
- menu.findItem(android.R.id.copy).setOnMenuItemClickListener(item -> copyToClipboard(context, activity.getSubject()));
+ menu.findItem(android.R.id.copy).setOnMenuItemClickListener(item -> ClipboardUtil.INSTANCE.copyToClipboard(context, activity.getSubject()));
});
- switch (ActivityType.findById(activity.getType())) {
+ final ActivityType type = ActivityType.findById(activity.getType());
+ setImageResource(binding.type, type);
+ setImageColor(context, binding.type, type);
+ }
+
+ private static void setImageResource(@NonNull ImageView imageView, @NonNull ActivityType type) {
+ switch (type) {
case CHANGE:
- binding.type.setImageResource(R.drawable.type_change_36dp);
+ imageView.setImageResource(R.drawable.type_change_36dp);
break;
case ADD:
- binding.type.setImageResource(R.drawable.type_add_color_36dp);
+ imageView.setImageResource(R.drawable.type_add_color_36dp);
break;
case DELETE:
- binding.type.setImageResource(R.drawable.type_delete_color_36dp);
+ imageView.setImageResource(R.drawable.type_delete_color_36dp);
break;
case ARCHIVE:
- binding.type.setImageResource(R.drawable.type_archive_grey600_36dp);
+ imageView.setImageResource(R.drawable.type_archive_grey600_36dp);
break;
case TAGGED_WITH_LABEL:
- binding.type.setImageResource(R.drawable.type_label_grey600_36dp);
+ imageView.setImageResource(R.drawable.type_label_grey600_36dp);
break;
case COMMENT:
- binding.type.setImageResource(R.drawable.type_comment_grey600_36dp);
+ imageView.setImageResource(R.drawable.type_comment_grey600_36dp);
break;
case FILES:
- binding.type.setImageResource(R.drawable.type_file_36dp);
+ imageView.setImageResource(R.drawable.type_file_36dp);
+ break;
case HISTORY:
- binding.type.setImageResource(R.drawable.type_file_36dp);
+ imageView.setImageResource(R.drawable.type_history_36dp);
+ break;
case DECK:
default:
+ imageView.setImageResource(R.drawable.ic_app_logo);
+ break;
+ }
+ }
+
+ private static void setImageColor(@NonNull Context context, @NonNull ImageView imageView, @NonNull ActivityType type) {
+ switch (type) {
+ case ADD:
+ ViewUtil.setImageColor(context, imageView, R.color.activity_create);
+ break;
+ case DELETE:
+ ViewUtil.setImageColor(context, imageView, R.color.activity_delete);
+ break;
+ default:
+ ViewUtil.setImageColor(context, imageView, R.color.grey600);
break;
}
}
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
new file mode 100644
index 000000000..34d2eb3f3
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/assignee/CardAssigneeDialog.java
@@ -0,0 +1,113 @@
+package it.niedermann.nextcloud.deck.ui.card.assignee;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.graphics.Color;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.DialogFragment;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.swiperefreshlayout.widget.CircularProgressDrawable;
+
+import com.bumptech.glide.Glide;
+
+import java.io.Serializable;
+
+import it.niedermann.nextcloud.deck.R;
+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;
+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 DialogPreviewBinding binding;
+ private EditCardViewModel viewModel;
+
+ @Nullable
+ private CardAssigneeListener cardAssigneeListener = null;
+ @SuppressWarnings("NotNullFieldNotInitialized")
+ @NonNull
+ private User user;
+
+ @Override
+ public void onAttach(@NonNull Context context) {
+ super.onAttach(context);
+
+ if (getParentFragment() instanceof CardAssigneeListener) {
+ this.cardAssigneeListener = (CardAssigneeListener) getParentFragment();
+ } else if (context instanceof CardAssigneeListener) {
+ this.cardAssigneeListener = (CardAssigneeListener) context;
+ }
+
+ final Bundle args = requireArguments();
+ if (!args.containsKey(KEY_USER)) {
+ throw new IllegalArgumentException("Provide at least " + KEY_USER);
+ }
+ final Serializable user = args.getSerializable(KEY_USER);
+ if (user == null) {
+ throw new IllegalArgumentException(KEY_USER + " must not be null.");
+ }
+ this.user = (User) user;
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ binding = DialogPreviewBinding.inflate(LayoutInflater.from(requireContext()));
+ viewModel = new ViewModelProvider(requireActivity()).get(EditCardViewModel.class);
+
+ AlertDialog.Builder dialogBuilder = new BrandedDeleteAlertDialogBuilder(requireContext());
+
+ if (viewModel.canEdit() && cardAssigneeListener != null) {
+ dialogBuilder.setPositiveButton(R.string.simple_unassign, (d, w) -> cardAssigneeListener.onUnassignUser(user));
+ }
+
+ return dialogBuilder
+ .setView(binding.getRoot())
+ .setNeutralButton(R.string.simple_close, null)
+ .create();
+ }
+
+ @Override
+ public void onActivityCreated(@Nullable Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ final Context context = requireContext();
+
+ final CircularProgressDrawable circularProgressDrawable = new CircularProgressDrawable(context);
+ circularProgressDrawable.setStrokeWidth(5f);
+ circularProgressDrawable.setCenterRadius(30f);
+ circularProgressDrawable.setColorSchemeColors(isDarkTheme(context) ? Color.LTGRAY : Color.DKGRAY);
+ circularProgressDrawable.start();
+
+ binding.avatar.post(() -> Glide.with(binding.avatar.getContext())
+ .load(viewModel.getAccount().getUrl() + "/index.php/avatar/" + Uri.encode(user.getUid()) + "/" + binding.avatar.getWidth())
+ .placeholder(circularProgressDrawable)
+ .error(R.drawable.ic_person_grey600_24dp)
+ .into(binding.avatar));
+ binding.title.setText(user.getDisplayname());
+ }
+
+ @Override
+ public void applyBrand(int mainColor) {
+ }
+
+ public static DialogFragment newInstance(@NonNull User user) {
+ final DialogFragment fragment = new CardAssigneeDialog();
+ final Bundle args = new Bundle();
+ args.putSerializable(KEY_USER, user);
+ fragment.setArguments(args);
+ return fragment;
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/assignee/CardAssigneeListener.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/assignee/CardAssigneeListener.java
new file mode 100644
index 000000000..259a8b57c
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/assignee/CardAssigneeListener.java
@@ -0,0 +1,11 @@
+package it.niedermann.nextcloud.deck.ui.card.assignee;
+
+import androidx.annotation.NonNull;
+
+import it.niedermann.nextcloud.deck.model.User;
+
+public interface CardAssigneeListener {
+
+ void onUnassignUser(@NonNull User user);
+
+} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/AttachmentDeletedListener.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/AttachmentDeletedListener.java
index c236fa4c5..2d3ece255 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/AttachmentDeletedListener.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/AttachmentDeletedListener.java
@@ -3,5 +3,5 @@ package it.niedermann.nextcloud.deck.ui.card.attachments;
import it.niedermann.nextcloud.deck.model.Attachment;
public interface AttachmentDeletedListener {
- void onAttachmentDeleted(Attachment attachment);
- } \ No newline at end of file
+ void onAttachmentDeleted(Attachment attachment);
+} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/AttachmentViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/AttachmentViewHolder.java
index ed3031b7c..533bbe322 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/AttachmentViewHolder.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/AttachmentViewHolder.java
@@ -1,18 +1,59 @@
package it.niedermann.nextcloud.deck.ui.card.attachments;
+import android.view.MenuInflater;
import android.view.View;
import android.widget.ImageView;
+import androidx.annotation.CallSuper;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.graphics.drawable.DrawableCompat;
+import androidx.fragment.app.FragmentManager;
import androidx.recyclerview.widget.RecyclerView;
+import it.niedermann.android.util.ClipboardUtil;
+import it.niedermann.nextcloud.deck.R;
+import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.deck.model.Attachment;
+import it.niedermann.nextcloud.deck.model.enums.DBStatus;
+import it.niedermann.nextcloud.deck.ui.branding.BrandingUtil;
+import it.niedermann.nextcloud.deck.util.AttachmentUtil;
+
public abstract class AttachmentViewHolder extends RecyclerView.ViewHolder {
AttachmentViewHolder(@NonNull View itemView) {
super(itemView);
}
+ public void bind(@NonNull Account account, @NonNull MenuInflater menuInflater, @NonNull FragmentManager fragmentManager, Long cardRemoteId, Attachment attachment, @Nullable View.OnClickListener onClickListener, @ColorInt int mainColor) {
+ bind(menuInflater, fragmentManager, cardRemoteId, attachment, onClickListener, mainColor, AttachmentUtil.getRemoteOrLocalUrl(account.getUrl(), cardRemoteId, attachment));
+ }
+
+ @CallSuper
+ public void bind(@NonNull MenuInflater menuInflater, @NonNull FragmentManager fragmentManager, Long cardRemoteId, Attachment attachment, @Nullable View.OnClickListener onClickListener, @ColorInt int mainColor, @Nullable String attachmentUri) {
+ setNotSyncedYetStatus(!DBStatus.LOCAL_EDITED.equals(attachment.getStatusEnum()), mainColor);
+ itemView.setOnCreateContextMenuListener((menu, v, menuInfo) -> {
+ menuInflater.inflate(R.menu.attachment_menu, menu);
+ menu.findItem(R.id.delete).setOnMenuItemClickListener(item -> {
+ DeleteAttachmentDialogFragment.newInstance(attachment).show(fragmentManager, DeleteAttachmentDialogFragment.class.getCanonicalName());
+ return false;
+ });
+ if (attachmentUri == null || attachment.getId() == null || cardRemoteId == null) {
+ menu.findItem(android.R.id.copyUrl).setVisible(false);
+ } else {
+ menu.findItem(android.R.id.copyUrl).setVisible(true);
+ menu.findItem(android.R.id.copyUrl).setOnMenuItemClickListener(item -> ClipboardUtil.INSTANCE.copyToClipboard(itemView.getContext(), attachment.getFilename(), attachmentUri));
+ }
+ });
+ }
+
abstract protected ImageView getPreview();
- abstract protected void setNotSyncedYetStatus(boolean synced, @ColorInt int color);
+ protected void setNotSyncedYetStatus(boolean synced, @ColorInt int mainColor) {
+ final ImageView notSyncedYet = getNotSyncedYetStatusIcon();
+ DrawableCompat.setTint(notSyncedYet.getDrawable(), BrandingUtil.getSecondaryForegroundColorDependingOnTheme(notSyncedYet.getContext(), mainColor));
+ notSyncedYet.setVisibility(synced ? View.GONE : View.VISIBLE);
+ }
+
+ abstract protected ImageView getNotSyncedYetStatusIcon();
} \ No newline at end of file
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 a72ed8ef2..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
@@ -3,9 +3,7 @@ package it.niedermann.nextcloud.deck.ui.card.attachments;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
-import android.net.Uri;
import android.os.Build;
-import android.text.format.Formatter;
import android.view.LayoutInflater;
import android.view.MenuInflater;
import android.view.View;
@@ -16,10 +14,10 @@ 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 com.bumptech.glide.Glide;
-
import java.util.ArrayList;
import java.util.List;
@@ -28,21 +26,23 @@ import it.niedermann.nextcloud.deck.databinding.ItemAttachmentDefaultBinding;
import it.niedermann.nextcloud.deck.databinding.ItemAttachmentImageBinding;
import it.niedermann.nextcloud.deck.model.Account;
import it.niedermann.nextcloud.deck.model.Attachment;
-import it.niedermann.nextcloud.deck.model.enums.DBStatus;
import it.niedermann.nextcloud.deck.ui.attachments.AttachmentsActivity;
-import it.niedermann.nextcloud.deck.util.AttachmentUtil;
-import it.niedermann.nextcloud.deck.util.DateUtil;
+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.ClipboardUtil.copyToClipboard;
+import static it.niedermann.nextcloud.deck.util.AttachmentUtil.openAttachmentInBrowser;
@SuppressWarnings("WeakerAccess")
-public class CardAttachmentAdapter extends RecyclerView.Adapter<AttachmentViewHolder> {
+public class CardAttachmentAdapter extends RecyclerView.Adapter<AttachmentViewHolder> implements Branded {
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;
@@ -51,17 +51,16 @@ public class CardAttachmentAdapter extends RecyclerView.Adapter<AttachmentViewHo
private Long cardRemoteId = null;
private final long cardLocalId;
@NonNull
- FragmentManager fragmentManager;
+ private final FragmentManager fragmentManager;
+ @NonNull
+ private final List<Attachment> attachments = new ArrayList<>();
@NonNull
- private List<Attachment> attachments = new ArrayList<>();
- @Nullable
private final AttachmentClickedListener attachmentClickedListener;
CardAttachmentAdapter(
- @NonNull Context context,
@NonNull FragmentManager fragmentManager,
@NonNull MenuInflater menuInflater,
- @Nullable AttachmentClickedListener attachmentClickedListener,
+ @NonNull AttachmentClickedListener attachmentClickedListener,
@NonNull Account account,
@Nullable Long cardLocalId
) {
@@ -95,39 +94,14 @@ public class CardAttachmentAdapter extends RecyclerView.Adapter<AttachmentViewHo
@Override
public void onBindViewHolder(@NonNull AttachmentViewHolder holder, int position) {
- final Context context = holder.itemView.getContext();
final Attachment attachment = attachments.get(position);
- final int viewType = getItemViewType(position);
-
- @Nullable final String uri = (attachment.getId() == null || cardRemoteId == null)
- ? attachment.getLocalPath() :
- AttachmentUtil.getRemoteUrl(account.getUrl(), cardRemoteId, attachment.getId());
- holder.setNotSyncedYetStatus(!DBStatus.LOCAL_EDITED.equals(attachment.getStatusEnum()), mainColor);
- holder.itemView.setOnCreateContextMenuListener((menu, v, menuInfo) -> {
- menuInflater.inflate(R.menu.attachment_menu, menu);
- menu.findItem(R.id.delete).setOnMenuItemClickListener(item -> {
- DeleteAttachmentDialogFragment.newInstance(attachment).show(fragmentManager, DeleteAttachmentDialogFragment.class.getCanonicalName());
- return false;
- });
- if (uri == null) {
- menu.findItem(android.R.id.copyUrl).setVisible(false);
- } else {
- menu.findItem(android.R.id.copyUrl).setOnMenuItemClickListener(item -> copyToClipboard(context, attachment.getFilename(), uri));
- }
- });
+ final Context context = holder.itemView.getContext();
+ final View.OnClickListener onClickListener;
- switch (viewType) {
+ switch (getItemViewType(position)) {
case VIEW_TYPE_IMAGE: {
- holder.getPreview().setImageResource(R.drawable.ic_image_grey600_24dp);
- Glide.with(context)
- .load(uri)
- .placeholder(R.drawable.ic_image_grey600_24dp)
- .error(R.drawable.ic_image_grey600_24dp)
- .into(holder.getPreview());
- holder.itemView.setOnClickListener((v) -> {
- if (attachmentClickedListener != null) {
- attachmentClickedListener.onAttachmentClicked(position);
- }
+ onClickListener = (event) -> {
+ attachmentClickedListener.onAttachmentClicked(position);
final Intent intent = AttachmentsActivity.createIntent(context, account, cardLocalId, attachment.getLocalId());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && context instanceof Activity) {
String transitionName = context.getString(R.string.transition_attachment_preview, String.valueOf(attachment.getLocalId()));
@@ -136,46 +110,16 @@ public class CardAttachmentAdapter extends RecyclerView.Adapter<AttachmentViewHo
} else {
context.startActivity(intent);
}
- });
+ };
break;
}
case VIEW_TYPE_DEFAULT:
default: {
- DefaultAttachmentViewHolder defaultHolder = (DefaultAttachmentViewHolder) holder;
-
- if (MimeTypeUtil.isAudio(attachment.getMimetype())) {
- holder.getPreview().setImageResource(R.drawable.ic_music_note_grey600_24dp);
- } else if (MimeTypeUtil.isVideo(attachment.getMimetype())) {
- holder.getPreview().setImageResource(R.drawable.ic_local_movies_grey600_24dp);
- } else if (MimeTypeUtil.isPdf(attachment.getMimetype())) {
- holder.getPreview().setImageResource(R.drawable.ic_baseline_picture_as_pdf_24);
- } else if (MimeTypeUtil.isContact(attachment.getMimetype())) {
- holder.getPreview().setImageResource(R.drawable.ic_baseline_contact_mail_24);
- } else {
- holder.getPreview().setImageResource(R.drawable.ic_attach_file_grey600_24dp);
- }
-
- if (cardRemoteId != null) {
- defaultHolder.itemView.setOnClickListener((event) -> {
- Intent openURL = new Intent(Intent.ACTION_VIEW);
- openURL.setData(Uri.parse(AttachmentUtil.getRemoteUrl(account.getUrl(), cardRemoteId, attachment.getId())));
- context.startActivity(openURL);
- });
- }
- defaultHolder.binding.filename.setText(attachment.getBasename());
- defaultHolder.binding.filesize.setText(Formatter.formatFileSize(context, attachment.getFilesize()));
- if (attachment.getLastModifiedLocal() != null) {
- defaultHolder.binding.modified.setText(DateUtil.getRelativeDateTimeString(context, attachment.getLastModifiedLocal().getTime()));
- defaultHolder.binding.modified.setVisibility(View.VISIBLE);
- } else if (attachment.getLastModified() != null) {
- defaultHolder.binding.modified.setText(DateUtil.getRelativeDateTimeString(context, attachment.getLastModified().getTime()));
- defaultHolder.binding.modified.setVisibility(View.VISIBLE);
- } else {
- defaultHolder.binding.modified.setVisibility(View.GONE);
- }
+ onClickListener = (event) -> openAttachmentInBrowser(context, account.getUrl(), cardRemoteId, attachment.getId());
break;
}
}
+ holder.bind(account, menuInflater, fragmentManager, cardRemoteId, attachment, onClickListener, mainColor);
}
@Override
@@ -188,21 +132,46 @@ 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
+ public void applyBrand(@ColorInt int mainColor) {
+ this.mainColor = mainColor;
+ notifyDataSetChanged();
}
}
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 c291c5bdf..07e8d39dd 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,35 +1,42 @@
package it.niedermann.nextcloud.deck.ui.card.attachments;
-import android.Manifest;
-import android.app.Activity;
import android.content.ContentResolver;
+import android.content.Context;
import android.content.Intent;
+import android.content.res.ColorStateList;
import android.net.Uri;
-import android.os.Build;
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.annotation.RequiresApi;
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;
import java.io.File;
import java.io.IOException;
-import java.util.Date;
+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;
@@ -41,10 +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;
@@ -53,14 +86,35 @@ 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 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;
- private static final int REQUEST_CODE_ADD_ATTACHMENT = 1;
- private static final int REQUEST_PERMISSION = 2;
+ @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
@@ -69,30 +123,58 @@ 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();
}
- syncManager = new SyncManager(requireContext());
adapter = new CardAttachmentAdapter(
- requireContext(),
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) {
@@ -106,7 +188,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() {
@@ -119,17 +201,19 @@ 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 (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && viewModel.canEdit()) {
+ if (editViewModel.canEdit()) {
binding.fab.setOnClickListener(v -> {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
- REQUEST_PERMISSION);
+ if (SDK_INT < LOLLIPOP) {
+ openNativeFilePicker();
} else {
- startFilePickerIntent();
+ binding.bottomNavigation.setSelectedItemId(R.id.gallery);
+ showGalleryPicker();
+ mBottomSheetBehaviour.setState(STATE_COLLAPSED);
+ backPressedCallback.setEnabled(true);
+ requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), backPressedCallback);
}
});
binding.fab.show();
@@ -146,116 +230,281 @@ public class CardAttachmentsFragment extends BrandedFragment implements Attachme
binding.fab.hide();
binding.emptyContentView.hideDescription();
}
+ @Nullable Context context = requireContext();
+ applyBrand(isBrandingEnabled(context)
+ ? readBrandMainColor(context)
+ : ContextCompat.getColor(context, R.color.defaultBrand));
return binding.getRoot();
}
- @RequiresApi(api = Build.VERSION_CODES.KITKAT)
- private void startFilePickerIntent() {
- Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
- intent.addCategory(Intent.CATEGORY_OPENABLE);
- intent.setType("*/*");
- startActivityForResult(intent, REQUEST_CODE_ADD_ATTACHMENT);
+ @Override
+ public void onPause() {
+ super.onPause();
+ backPressedCallback.setEnabled(false);
}
@Override
- public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
- super.onActivityResult(requestCode, resultCode, data);
- if (requestCode == REQUEST_CODE_ADD_ATTACHMENT && resultCode == Activity.RESULT_OK) {
- if (data == null) {
- ExceptionDialogFragment.newInstance(new UploadAttachmentFailedException("Intent data is null"), viewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
- return;
+ 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);
}
- final Uri sourceUri = data.getData();
- if (sourceUri == null) {
- ExceptionDialogFragment.newInstance(new UploadAttachmentFailedException("sourceUri is null"), viewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
- return;
+ }
+ }
+
+ 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);
}
- if (!ContentResolver.SCHEME_CONTENT.equals(sourceUri.getScheme())) {
- ExceptionDialogFragment.newInstance(new UploadAttachmentFailedException("Unknown URI scheme: " + sourceUri.getScheme()), viewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
- return;
+ }
+ }
+
+ 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);
+ }
}
+ }
+ }
- DeckLog.verbose("--- found content URL " + sourceUri.getPath());
- File fileToUpload;
+ private void openNativeCameraPicker() {
+ if (SDK_INT >= LOLLIPOP) {
+ startActivityForResult(TakePhotoActivity.createIntent(requireContext()), REQUEST_CODE_PICK_CAMERA);
+ } else {
+ ExceptionDialogFragment.newInstance(new UnsupportedOperationException("This feature requires Android 5"), editViewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ }
+ }
- 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());
- return;
- }
+ 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);
+ }
+ }
- 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;
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ switch (requestCode) {
+ case REQUEST_CODE_PICK_CONTACT:
+ case REQUEST_CODE_PICK_CAMERA:
+ case REQUEST_CODE_PICK_FILE: {
+ if (resultCode == RESULT_OK) {
+ 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);
}
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ if (this.pickerAdapter != null) {
+ this.pickerAdapter.onDestroy();
+ this.binding.pickerRecyclerView.setAdapter(null);
+ }
+ super.onDestroy();
+ }
- final Date now = new Date();
- 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();
+ 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;
+ }
+ }
+ 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 = editViewModel.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);
+ 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());
+ }
} else {
- ExceptionDialogFragment.newInstance(new UploadAttachmentFailedException("Unknown URI scheme", t), viewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ editViewModel.getFullCard().getAttachments().remove(a);
+ editViewModel.getFullCard().getAttachments().add(0, next);
+ adapter.replaceAttachment(a, next);
}
- } else {
- viewModel.getFullCard().getAttachments().remove(a);
- adapter.removeAttachment(a);
- viewModel.getFullCard().getAttachments().add(next);
- adapter.addAttachment(next);
- }
- });
+ });
+ }
+ break;
+ }
+ default: {
+ throw new UploadAttachmentFailedException("Unknown URI scheme: " + sourceUri.getScheme());
}
- updateEmptyContentView();
}
-
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
- if (requestCode == REQUEST_PERMISSION) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
- startFilePickerIntent();
+ switch (requestCode) {
+ case REQUEST_CODE_PICK_FILE_PERMISSION: {
+ if (checkSelfPermission(requireActivity(), READ_EXTERNAL_STORAGE) == PERMISSION_GRANTED) {
+ showFilePicker();
+ } else {
+ Toast.makeText(requireContext(), R.string.cannot_upload_files_without_permission, Toast.LENGTH_LONG).show();
+ }
+ break;
}
- } else {
- super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ 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);
}
}
- public static Fragment newInstance() {
- return new CardAttachmentsFragment();
- }
-
@Override
public void onAttachmentDeleted(Attachment attachment) {
adapter.removeAttachment(attachment);
- viewModel.getFullCard().getAttachments().remove(attachment);
- if (!viewModel.isCreateMode() && attachment.getLocalId() != null) {
- 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 = editViewModel.deleteAttachmentOfCard(editViewModel.getAccount().getId(), editViewModel.getFullCard().getLocalId(), attachment.getLocalId());
+ observeOnce(deleteLiveData, this, (next) -> {
+ if (deleteLiveData.hasError() && !SyncManager.ignoreExceptionOnVoidError(deleteLiveData.getError())) {
+ ExceptionDialogFragment.newInstance(deleteLiveData.getError(), editViewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ }
+ });
}
- updateEmptyContentView();
}
@Override
@@ -263,19 +512,28 @@ 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() {
+ return new CardAttachmentsFragment();
}
}
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 7acdd390e..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
@@ -1,15 +1,25 @@
package it.niedermann.nextcloud.deck.ui.card.attachments;
+import android.text.format.Formatter;
+import android.view.MenuInflater;
import android.view.View;
import android.widget.ImageView;
import androidx.annotation.ColorInt;
-import androidx.core.graphics.drawable.DrawableCompat;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.FragmentManager;
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.DateUtil;
+
+import static it.niedermann.nextcloud.deck.util.AttachmentUtil.getIconForMimeType;
+import static it.niedermann.nextcloud.deck.util.AttachmentUtil.openAttachmentInBrowser;
public class DefaultAttachmentViewHolder extends AttachmentViewHolder {
- ItemAttachmentDefaultBinding binding;
+ private final ItemAttachmentDefaultBinding binding;
@SuppressWarnings("WeakerAccess")
public DefaultAttachmentViewHolder(ItemAttachmentDefaultBinding binding) {
@@ -23,8 +33,24 @@ public class DefaultAttachmentViewHolder extends AttachmentViewHolder {
}
@Override
- protected void setNotSyncedYetStatus(boolean synced, @ColorInt int mainColor) {
- DrawableCompat.setTint(binding.notSyncedYet.getDrawable(), mainColor);
- binding.notSyncedYet.setVisibility(synced ? View.GONE : View.VISIBLE);
+ protected ImageView getNotSyncedYetStatusIcon() {
+ return binding.notSyncedYet;
+ }
+
+ 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);
+ 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) {
+ binding.modified.setText(DateUtil.getRelativeDateTimeString(binding.modified.getContext(), attachment.getLastModifiedLocal().toEpochMilli()));
+ binding.modified.setVisibility(View.VISIBLE);
+ } else if (attachment.getLastModified() != null) {
+ binding.modified.setText(DateUtil.getRelativeDateTimeString(binding.modified.getContext(), attachment.getLastModified().toEpochMilli()));
+ binding.modified.setVisibility(View.VISIBLE);
+ } else {
+ binding.modified.setVisibility(View.GONE);
+ }
}
} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/ImageAttachmentViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/ImageAttachmentViewHolder.java
index d13675a30..3c95da1b7 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/ImageAttachmentViewHolder.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/ImageAttachmentViewHolder.java
@@ -1,15 +1,24 @@
package it.niedermann.nextcloud.deck.ui.card.attachments;
+import android.view.MenuInflater;
import android.view.View;
import android.widget.ImageView;
import androidx.annotation.ColorInt;
-import androidx.core.graphics.drawable.DrawableCompat;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.FragmentManager;
+import com.bumptech.glide.Glide;
+
+import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.ItemAttachmentImageBinding;
+import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.deck.model.Attachment;
+import it.niedermann.nextcloud.deck.util.AttachmentUtil;
public class ImageAttachmentViewHolder extends AttachmentViewHolder {
- private ItemAttachmentImageBinding binding;
+ private final ItemAttachmentImageBinding binding;
@SuppressWarnings("WeakerAccess")
public ImageAttachmentViewHolder(ItemAttachmentImageBinding binding) {
@@ -23,8 +32,22 @@ public class ImageAttachmentViewHolder extends AttachmentViewHolder {
}
@Override
- protected void setNotSyncedYetStatus(boolean synced, @ColorInt int mainColor) {
- DrawableCompat.setTint(binding.notSyncedYet.getDrawable(), mainColor);
- binding.notSyncedYet.setVisibility(synced ? View.GONE : View.VISIBLE);
+ protected ImageView getNotSyncedYetStatusIcon() {
+ return binding.notSyncedYet;
+ }
+
+ 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(menuInflater, fragmentManager, cardRemoteId, attachment, onClickListener, mainColor, AttachmentUtil.getRemoteOrLocalUrl(account.getUrl(), cardRemoteId, attachment));
+
+ getPreview().post(() -> {
+ @Nullable final String uri = AttachmentUtil.getThumbnailUrl(account.getServerDeckVersionAsObject(), account.getUrl(), cardRemoteId, attachment, getPreview().getWidth());
+ Glide.with(getPreview().getContext())
+ .load(uri)
+ .placeholder(R.drawable.ic_image_grey600_24dp)
+ .error(R.drawable.ic_image_grey600_24dp)
+ .into(getPreview());
+ });
+
+ itemView.setOnClickListener(onClickListener);
}
} \ No newline at end of file
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/card/comments/CardCommentsFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsFragment.java
index e261c37a2..3fd536aa6 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsFragment.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsFragment.java
@@ -15,7 +15,7 @@ import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
-import java.util.Date;
+import java.time.Instant;
import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.R;
@@ -23,12 +23,15 @@ import it.niedermann.nextcloud.deck.databinding.FragmentCardEditTabCommentsBindi
import it.niedermann.nextcloud.deck.model.ocs.comment.DeckComment;
import it.niedermann.nextcloud.deck.model.ocs.comment.full.FullDeckComment;
import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
+import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.WrappedLiveData;
import it.niedermann.nextcloud.deck.ui.branding.BrandedFragment;
import it.niedermann.nextcloud.deck.ui.card.EditActivity;
import it.niedermann.nextcloud.deck.ui.card.EditCardViewModel;
+import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
+import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce;
import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.applyBrandToEditText;
import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.applyBrandToFAB;
import static it.niedermann.nextcloud.deck.util.ViewUtil.setupMentions;
@@ -38,7 +41,6 @@ public class CardCommentsFragment extends BrandedFragment implements CommentEdit
private FragmentCardEditTabCommentsBinding binding;
private EditCardViewModel mainViewModel;
private CommentsViewModel commentsViewModel;
- private SyncManager syncManager;
private CardCommentsAdapter adapter;
public static Fragment newInstance() {
@@ -68,7 +70,6 @@ public class CardCommentsFragment extends BrandedFragment implements CommentEdit
commentsViewModel = new ViewModelProvider(this).get(CommentsViewModel.class);
- syncManager = new SyncManager(requireActivity());
adapter = new CardCommentsAdapter(requireContext(), mainViewModel.getAccount(), requireActivity().getMenuInflater(), this, this, getChildFragmentManager());
binding.comments.setAdapter(adapter);
@@ -82,7 +83,7 @@ public class CardCommentsFragment extends BrandedFragment implements CommentEdit
setupMentions(mainViewModel.getAccount(), comment.getComment().getMentions(), binding.replyCommentText);
}
});
- syncManager.getFullCommentsForLocalCardId(mainViewModel.getFullCard().getLocalId()).observe(getViewLifecycleOwner(),
+ commentsViewModel.getFullCommentsForLocalCardId(mainViewModel.getFullCard().getLocalId()).observe(getViewLifecycleOwner(),
(comments) -> {
if (comments != null && comments.size() > 0) {
binding.emptyContentView.setVisibility(GONE);
@@ -100,13 +101,13 @@ public class CardCommentsFragment extends BrandedFragment implements CommentEdit
if (!TextUtils.isEmpty(binding.message.getText().toString().trim())) {
binding.emptyContentView.setVisibility(GONE);
binding.comments.setVisibility(VISIBLE);
- final DeckComment comment = new DeckComment(binding.message.getText().toString().trim(), mainViewModel.getAccount().getUserName(), new Date());
+ final DeckComment comment = new DeckComment(binding.message.getText().toString().trim(), mainViewModel.getAccount().getUserName(), Instant.now());
final FullDeckComment parent = commentsViewModel.getReplyToComment().getValue();
if (parent != null) {
comment.setParentId(parent.getId());
commentsViewModel.setReplyToComment(null);
}
- syncManager.addCommentToCard(mainViewModel.getAccount().getId(), mainViewModel.getFullCard().getLocalId(), comment);
+ commentsViewModel.addCommentToCard(mainViewModel.getAccount().getId(), mainViewModel.getFullCard().getLocalId(), comment);
}
binding.message.setText(null);
});
@@ -116,6 +117,7 @@ public class CardCommentsFragment extends BrandedFragment implements CommentEdit
}
return true;
});
+ binding.message.addTextChangedListener(new CardCommentsMentionProposer(getViewLifecycleOwner(), mainViewModel.getAccount(), mainViewModel.getBoardId(), binding.message, binding.mentionProposerWrapper, binding.mentionProposer));
} else {
binding.addCommentLayout.setVisibility(GONE);
}
@@ -133,12 +135,17 @@ public class CardCommentsFragment extends BrandedFragment implements CommentEdit
@Override
public void onCommentEdited(Long id, String comment) {
- syncManager.updateComment(mainViewModel.getAccount().getId(), mainViewModel.getFullCard().getLocalId(), id, comment);
+ commentsViewModel.updateComment(mainViewModel.getAccount().getId(), mainViewModel.getFullCard().getLocalId(), id, comment);
}
@Override
public void onCommentDeleted(Long localId) {
- syncManager.deleteComment(mainViewModel.getAccount().getId(), mainViewModel.getFullCard().getLocalId(), localId);
+ final WrappedLiveData<Void> deleteLiveData = commentsViewModel.deleteComment(mainViewModel.getAccount().getId(), mainViewModel.getFullCard().getLocalId(), localId);
+ observeOnce(deleteLiveData, this, (next) -> {
+ if (deleteLiveData.hasError() && !SyncManager.ignoreExceptionOnVoidError(deleteLiveData.getError())) {
+ ExceptionDialogFragment.newInstance(deleteLiveData.getError(), mainViewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ }
+ });
}
@Override
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsMentionProposer.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsMentionProposer.java
new file mode 100644
index 000000000..7ca7a6384
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsMentionProposer.java
@@ -0,0 +1,139 @@
+package it.niedermann.nextcloud.deck.ui.card.comments;
+
+import android.annotation.SuppressLint;
+import android.net.Uri;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+
+import androidx.annotation.NonNull;
+import androidx.core.util.Pair;
+import androidx.lifecycle.LifecycleOwner;
+
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.request.RequestOptions;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import it.niedermann.android.util.DimensionUtil;
+import it.niedermann.nextcloud.deck.R;
+import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.deck.model.User;
+import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
+import it.niedermann.nextcloud.deck.ui.card.comments.util.CommentsUtil;
+
+import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce;
+
+public class CardCommentsMentionProposer implements TextWatcher {
+
+ private final int avatarSize;
+ @NonNull
+ private final SyncManager syncManager;
+ @NonNull
+ private final LinearLayout.LayoutParams layoutParams;
+ @NonNull
+ private final LifecycleOwner owner;
+ @NonNull
+ private final Account account;
+ private final long boardLocalId;
+ @NonNull
+ private final EditText editText;
+ @NonNull
+ private final LinearLayout mentionProposer;
+ @NonNull
+ private final LinearLayout mentionProposerWrapper;
+
+ @NonNull
+ private final List<User> users = new ArrayList<>();
+
+ public CardCommentsMentionProposer(@NonNull LifecycleOwner owner, @NonNull Account account, long boardLocalId, @NonNull EditText editText, LinearLayout mentionProposerWrapper, @NonNull LinearLayout avatarProposer) {
+ this.owner = owner;
+ this.account = account;
+ this.boardLocalId = boardLocalId;
+ this.editText = editText;
+ this.mentionProposerWrapper = mentionProposerWrapper;
+ this.mentionProposer = avatarProposer;
+ syncManager = new SyncManager(editText.getContext());
+ avatarSize = DimensionUtil.INSTANCE.dpToPx(mentionProposer.getContext(), R.dimen.avatar_size_small);
+ layoutParams = new LinearLayout.LayoutParams(avatarSize, avatarSize);
+ layoutParams.setMarginEnd(DimensionUtil.INSTANCE.dpToPx(mentionProposer.getContext(), R.dimen.spacer_1x));
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ final int selectionStart = editText.getSelectionStart();
+ final int selectionEnd = editText.getSelectionEnd();
+ final Pair<String, Integer> mentionProposal = CommentsUtil.getUserNameForMentionProposal(s.toString(), selectionStart);
+ if (mentionProposal == null || (mentionProposal.first != null && mentionProposal.first.length() == 0) || selectionStart != selectionEnd) {
+ mentionProposer.removeAllViews();
+ mentionProposerWrapper.setVisibility(View.GONE);
+ this.users.clear();
+ } else {
+ if (mentionProposal.first != null && mentionProposal.second != null) {
+ observeOnce(syncManager.searchUserByUidOrDisplayName(account.getId(), boardLocalId, -1L, mentionProposal.first), owner, (users) -> {
+ if (!users.equals(this.users)) {
+ mentionProposer.removeAllViews();
+ if (users.size() > 0) {
+ mentionProposerWrapper.setVisibility(View.VISIBLE);
+ for (User user : users) {
+ final ImageView avatar = new ImageView(mentionProposer.getContext());
+ avatar.setLayoutParams(layoutParams);
+ updateListenerOfView(avatar, s, mentionProposal, user);
+
+ mentionProposer.addView(avatar);
+
+ Glide.with(avatar.getContext())
+ .load(account.getUrl() + "/index.php/avatar/" + Uri.encode(user.getUid()) + "/" + avatarSize)
+ .placeholder(R.drawable.ic_person_grey600_24dp)
+ .error(R.drawable.ic_person_grey600_24dp)
+ .apply(RequestOptions.circleCropTransform())
+ .into(avatar);
+ }
+ } else {
+ mentionProposerWrapper.setVisibility(View.GONE);
+ }
+ this.users.clear();
+ this.users.addAll(users);
+ } else {
+ int i = 0;
+ for (User user : users) {
+ updateListenerOfView(mentionProposer.getChildAt(i), s, mentionProposal, user);
+ i++;
+ }
+ }
+ });
+ } else {
+ this.users.clear();
+ mentionProposer.removeAllViews();
+ mentionProposerWrapper.setVisibility(View.GONE);
+ }
+ }
+ }
+
+ @SuppressLint("SetTextI18n")
+ private void updateListenerOfView(View avatar, CharSequence s, Pair<String, Integer> mentionProposal, User user) {
+ avatar.setOnClickListener((c) -> {
+ editText.setText(
+ s.subSequence(0, mentionProposal.second) +
+ user.getUid() +
+ s.subSequence(mentionProposal.second + mentionProposal.first.length(), s.length())
+ );
+ editText.setSelection(mentionProposal.second + user.getUid().length());
+ mentionProposerWrapper.setVisibility(View.GONE);
+ });
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CommentsViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CommentsViewModel.java
index f7fd247a9..dada94d5b 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CommentsViewModel.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CommentsViewModel.java
@@ -1,15 +1,30 @@
package it.niedermann.nextcloud.deck.ui.card.comments;
+import android.app.Application;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
-import androidx.lifecycle.ViewModel;
+import java.util.List;
+
+import it.niedermann.nextcloud.deck.model.ocs.comment.DeckComment;
import it.niedermann.nextcloud.deck.model.ocs.comment.full.FullDeckComment;
+import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
+import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.WrappedLiveData;
@SuppressWarnings("WeakerAccess")
-public class CommentsViewModel extends ViewModel {
+public class CommentsViewModel extends AndroidViewModel {
- private MutableLiveData<FullDeckComment> replyToComment = new MutableLiveData<>();
+ private final SyncManager syncManager;
+
+ private final MutableLiveData<FullDeckComment> replyToComment = new MutableLiveData<>();
+
+ public CommentsViewModel(@NonNull Application application) {
+ super(application);
+ this.syncManager = new SyncManager(application);
+ }
public void setReplyToComment(FullDeckComment replyToComment) {
this.replyToComment.postValue(replyToComment);
@@ -18,4 +33,20 @@ public class CommentsViewModel extends ViewModel {
public LiveData<FullDeckComment> getReplyToComment() {
return this.replyToComment;
}
+
+ public LiveData<List<FullDeckComment>> getFullCommentsForLocalCardId(long localCardId) {
+ return syncManager.getFullCommentsForLocalCardId(localCardId);
+ }
+
+ public void addCommentToCard(long accountId, long cardId, @NonNull DeckComment comment) {
+ syncManager.addCommentToCard(accountId, cardId, comment);
+ }
+
+ public void updateComment(long accountId, long localCardId, long localCommentId, String comment) {
+ syncManager.updateComment(accountId, localCardId, localCommentId, comment);
+ }
+
+ public WrappedLiveData<Void> deleteComment(long accountId, long localCardId, long localCommentId) {
+ return syncManager.deleteComment(accountId, localCardId, localCommentId);
+ }
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/ItemCommentViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/ItemCommentViewHolder.java
index 086d799af..3e540c95e 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/ItemCommentViewHolder.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/ItemCommentViewHolder.java
@@ -11,22 +11,25 @@ import androidx.core.graphics.drawable.DrawableCompat;
import androidx.fragment.app.FragmentManager;
import androidx.recyclerview.widget.RecyclerView;
-import java.text.DateFormat;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.time.format.FormatStyle;
+import it.niedermann.android.util.ClipboardUtil;
+import it.niedermann.android.util.DimensionUtil;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.ItemCommentBinding;
import it.niedermann.nextcloud.deck.model.Account;
import it.niedermann.nextcloud.deck.model.enums.DBStatus;
import it.niedermann.nextcloud.deck.model.ocs.comment.full.FullDeckComment;
import it.niedermann.nextcloud.deck.util.DateUtil;
-import it.niedermann.nextcloud.deck.util.DimensionUtil;
import it.niedermann.nextcloud.deck.util.ViewUtil;
-import static it.niedermann.nextcloud.deck.util.ClipboardUtil.copyToClipboard;
import static it.niedermann.nextcloud.deck.util.ViewUtil.setupMentions;
public class ItemCommentViewHolder extends RecyclerView.ViewHolder {
- private ItemCommentBinding binding;
+ private final ItemCommentBinding binding;
+ private final DateTimeFormatter dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM);
@SuppressWarnings("WeakerAccess")
public ItemCommentViewHolder(ItemCommentBinding binding) {
@@ -35,15 +38,15 @@ public class ItemCommentViewHolder extends RecyclerView.ViewHolder {
}
public void bind(@NonNull FullDeckComment comment, @NonNull Account account, @ColorInt int mainColor, @NonNull MenuInflater inflater, @NonNull CommentDeletedListener deletedListener, @NonNull CommentSelectAsReplyListener selectAsReplyListener, @NonNull FragmentManager fragmentManager) {
- ViewUtil.addAvatar(binding.avatar, account.getUrl(), comment.getComment().getActorId(), DimensionUtil.dpToPx(binding.avatar.getContext(), R.dimen.icon_size_details), R.drawable.ic_person_grey600_24dp);
+ ViewUtil.addAvatar(binding.avatar, account.getUrl(), comment.getComment().getActorId(), DimensionUtil.INSTANCE.dpToPx(binding.avatar.getContext(), R.dimen.icon_size_details), R.drawable.ic_person_grey600_24dp);
binding.message.setText(comment.getComment().getMessage());
binding.actorDisplayName.setText(comment.getComment().getActorDisplayName());
- binding.creationDateTime.setText(DateUtil.getRelativeDateTimeString(binding.creationDateTime.getContext(), comment.getComment().getCreationDateTime().getTime()));
+ binding.creationDateTime.setText(DateUtil.getRelativeDateTimeString(binding.creationDateTime.getContext(), comment.getComment().getCreationDateTime().toEpochMilli()));
itemView.setOnClickListener(View::showContextMenu);
itemView.setOnCreateContextMenuListener((menu, v, menuInfo) -> {
inflater.inflate(R.menu.comment_menu, menu);
- menu.findItem(android.R.id.copy).setOnMenuItemClickListener(item -> copyToClipboard(itemView.getContext(), comment.getComment().getMessage()));
+ menu.findItem(android.R.id.copy).setOnMenuItemClickListener(item -> ClipboardUtil.INSTANCE.copyToClipboard(itemView.getContext(), comment.getComment().getMessage()));
final MenuItem replyMenuItem = menu.findItem(R.id.reply);
if (comment.getStatusEnum() != DBStatus.LOCAL_EDITED && account.getServerDeckVersionAsObject().supportsCommentsReplys()) {
replyMenuItem.setOnMenuItemClickListener(item -> {
@@ -72,7 +75,7 @@ public class ItemCommentViewHolder extends RecyclerView.ViewHolder {
DrawableCompat.setTint(binding.notSyncedYet.getDrawable(), mainColor);
binding.notSyncedYet.setVisibility(DBStatus.LOCAL_EDITED.equals(comment.getStatusEnum()) ? View.VISIBLE : View.GONE);
- TooltipCompat.setTooltipText(binding.creationDateTime, DateFormat.getDateTimeInstance().format(comment.getComment().getCreationDateTime()));
+ TooltipCompat.setTooltipText(binding.creationDateTime, comment.getComment().getCreationDateTime().atZone(ZoneId.systemDefault()).format(dateFormatter));
setupMentions(account, comment.getComment().getMentions(), binding.message);
if (comment.getParent() == null) {
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/util/CommentsUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/util/CommentsUtil.java
new file mode 100644
index 000000000..5251291c8
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/util/CommentsUtil.java
@@ -0,0 +1,48 @@
+package it.niedermann.nextcloud.deck.ui.card.comments.util;
+
+
+import androidx.core.util.Pair;
+
+public class CommentsUtil {
+
+ public static Pair<String, Integer> getUserNameForMentionProposal(String text, int cursorPosition) {
+ Pair result = null;
+
+ if (text != null) {
+ // find start of relevant substring
+ int cursor = cursorPosition;
+ if (cursor < 1) {
+ return null;
+ }
+ int start = 0;
+ while (cursor > 0) {
+ cursor--;
+ if (Character.isWhitespace(text.charAt(cursor))) {
+ start = cursor + 1;
+ break;
+ }
+ }
+ if (text.length()-1 < start || text.charAt(start) != '@') {
+ return null;
+ }
+
+ // find end of relevant substring
+ cursor = cursorPosition;
+ int textLength = text.length();
+ int end = textLength;
+ while (cursor < textLength) {
+ if (Character.isWhitespace(text.charAt(cursor))) {
+ end = cursor;
+ break;
+ }
+ cursor++;
+ }
+
+ start++;
+ result = Pair.create(text.substring(start, end), start);
+
+ }
+
+ return result;
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/AssigneeAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/AssigneeAdapter.java
new file mode 100644
index 000000000..aa8c3e8f6
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/AssigneeAdapter.java
@@ -0,0 +1,80 @@
+package it.niedermann.nextcloud.deck.ui.card.details;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.core.util.Consumer;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import it.niedermann.nextcloud.deck.databinding.ItemAssigneeBinding;
+import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.deck.model.User;
+
+import static androidx.recyclerview.widget.RecyclerView.NO_ID;
+
+@SuppressWarnings("WeakerAccess")
+public class AssigneeAdapter extends RecyclerView.Adapter<AssigneeViewHolder> {
+
+ private final Account account;
+ @NonNull
+ private List<User> users = new ArrayList<>();
+ @NonNull
+ private final Consumer<User> userClickedListener;
+
+ AssigneeAdapter(
+ @NonNull Consumer<User> userClickedListener,
+ @NonNull Account account
+ ) {
+ super();
+ this.userClickedListener = userClickedListener;
+ this.account = account;
+ setHasStableIds(true);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ Long id = users.get(position).getLocalId();
+ return id == null ? NO_ID : id;
+ }
+
+ @NonNull
+ @Override
+ public AssigneeViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ final Context context = parent.getContext();
+ return new AssigneeViewHolder(ItemAssigneeBinding.inflate(LayoutInflater.from(context)));
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull AssigneeViewHolder holder, int position) {
+ final User user = users.get(position);
+ holder.bind(account, user, userClickedListener);
+ }
+
+ @Override
+ public int getItemCount() {
+ return users.size();
+ }
+
+ public void setUsers(@NonNull List<User> users) {
+ this.users.clear();
+ this.users.addAll(users);
+ notifyDataSetChanged();
+ }
+
+ public void addUser(@NonNull User user) {
+ this.users.add(user);
+ notifyItemInserted(this.users.size());
+ }
+
+ public void removeUser(@NonNull User user) {
+ final int index = this.users.indexOf(user);
+ this.users.remove(user);
+ notifyItemRemoved(index);
+ }
+
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/AssigneeDecoration.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/AssigneeDecoration.java
new file mode 100644
index 000000000..096dcfa53
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/AssigneeDecoration.java
@@ -0,0 +1,28 @@
+package it.niedermann.nextcloud.deck.ui.card.details;
+
+import android.graphics.Rect;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Px;
+import androidx.recyclerview.widget.RecyclerView;
+
+public class AssigneeDecoration extends RecyclerView.ItemDecoration {
+
+ private final int gutter;
+
+ public AssigneeDecoration(@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) {
+ // All columns get some spacing at the bottom and at the right side
+ outRect.right = gutter;
+ outRect.bottom = gutter;
+ }
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/AssigneeViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/AssigneeViewHolder.java
new file mode 100644
index 000000000..ddb1236b6
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/AssigneeViewHolder.java
@@ -0,0 +1,29 @@
+package it.niedermann.nextcloud.deck.ui.card.details;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.util.Consumer;
+import androidx.recyclerview.widget.RecyclerView;
+
+import it.niedermann.nextcloud.deck.R;
+import it.niedermann.nextcloud.deck.databinding.ItemAssigneeBinding;
+import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.deck.model.User;
+import it.niedermann.nextcloud.deck.util.ViewUtil;
+
+public class AssigneeViewHolder extends RecyclerView.ViewHolder {
+ private ItemAssigneeBinding binding;
+
+ @SuppressWarnings("WeakerAccess")
+ public AssigneeViewHolder(ItemAssigneeBinding binding) {
+ super(binding.getRoot());
+ this.binding = binding;
+ }
+
+ public void bind(@NonNull Account account, @NonNull User user, @Nullable Consumer<User> onClickListener) {
+ ViewUtil.addAvatar(binding.avatar, account.getUrl(), user.getUid(), R.drawable.ic_person_grey600_24dp);
+ if(onClickListener != null) {
+ itemView.setOnClickListener((v) -> onClickListener.accept(user));
+ }
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardDetailsFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardDetailsFragment.java
index 3182fffa2..2c697de08 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardDetailsFragment.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardDetailsFragment.java
@@ -2,25 +2,25 @@ package it.niedermann.nextcloud.deck.ui.card.details;
import android.content.Context;
import android.content.res.ColorStateList;
-import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.text.Editable;
+import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
-import android.widget.ImageView;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.Px;
import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.content.ContextCompat;
import androidx.core.graphics.ColorUtils;
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
+import androidx.recyclerview.widget.GridLayoutManager;
import com.google.android.material.chip.Chip;
import com.google.android.material.snackbar.Snackbar;
@@ -31,18 +31,21 @@ import com.wdullaer.materialdatetimepicker.time.TimePickerDialog.OnTimeSetListen
import com.yydcdut.markdown.MarkdownProcessor;
import com.yydcdut.markdown.syntax.edit.EditFactory;
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
-import java.util.Calendar;
-import java.util.Date;
-import java.util.Locale;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.FormatStyle;
+import it.niedermann.android.util.ColorUtil;
+import it.niedermann.android.util.DimensionUtil;
import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.FragmentCardEditTabDetailsBinding;
import it.niedermann.nextcloud.deck.model.Label;
import it.niedermann.nextcloud.deck.model.User;
-import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.WrappedLiveData;
import it.niedermann.nextcloud.deck.ui.branding.BrandedDatePickerDialog;
import it.niedermann.nextcloud.deck.ui.branding.BrandedFragment;
@@ -51,26 +54,23 @@ import it.niedermann.nextcloud.deck.ui.branding.BrandedTimePickerDialog;
import it.niedermann.nextcloud.deck.ui.card.EditCardViewModel;
import it.niedermann.nextcloud.deck.ui.card.LabelAutoCompleteAdapter;
import it.niedermann.nextcloud.deck.ui.card.UserAutoCompleteAdapter;
+import it.niedermann.nextcloud.deck.ui.card.assignee.CardAssigneeDialog;
+import it.niedermann.nextcloud.deck.ui.card.assignee.CardAssigneeListener;
import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment;
-import it.niedermann.nextcloud.deck.util.ColorUtil;
import it.niedermann.nextcloud.deck.util.MarkDownUtil;
-import it.niedermann.nextcloud.deck.util.ViewUtil;
-import static android.text.format.DateFormat.getDateFormat;
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce;
import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.applyBrandToEditText;
-import static it.niedermann.nextcloud.deck.util.DimensionUtil.dpToPx;
-public class CardDetailsFragment extends BrandedFragment implements OnDateSetListener, OnTimeSetListener {
+public class CardDetailsFragment extends BrandedFragment implements OnDateSetListener, OnTimeSetListener, CardAssigneeListener {
private FragmentCardEditTabDetailsBinding binding;
private EditCardViewModel viewModel;
- private SyncManager syncManager;
- private DateFormat dateFormat;
- private DateFormat dueTime = new SimpleDateFormat("HH:mm", Locale.ROOT);
- @Px
- private int avatarSize;
- private LinearLayout.LayoutParams avatarLayoutParams;
+ private AssigneeAdapter adapter;
+ private final DateTimeFormatter dateFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM);
+ private final DateTimeFormatter timeFormatter = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT);
private AppCompatActivity activity;
@Override
@@ -92,8 +92,6 @@ public class CardDetailsFragment extends BrandedFragment implements OnDateSetLis
ViewGroup container,
Bundle savedInstanceState) {
binding = FragmentCardEditTabDetailsBinding.inflate(inflater, container, false);
- dateFormat = getDateFormat(activity);
-
viewModel = new ViewModelProvider(activity).get(EditCardViewModel.class);
// This might be a zombie fragment with an empty EditCardViewModel after Android killed the activity (but not the fragment instance
@@ -103,22 +101,20 @@ public class CardDetailsFragment extends BrandedFragment implements OnDateSetLis
return binding.getRoot();
}
- syncManager = new SyncManager(requireContext());
-
- avatarSize = dpToPx(requireContext(), R.dimen.avatar_size);
- avatarLayoutParams = new LinearLayout.LayoutParams(avatarSize, avatarSize);
- avatarLayoutParams.setMargins(0, 0, dpToPx(requireContext(), R.dimen.spacer_1x), 0);
+ @Px final int avatarSize = DimensionUtil.INSTANCE.dpToPx(requireContext(), R.dimen.avatar_size);
+ final LinearLayout.LayoutParams avatarLayoutParams = new LinearLayout.LayoutParams(avatarSize, avatarSize);
+ avatarLayoutParams.setMargins(0, 0, DimensionUtil.INSTANCE.dpToPx(requireContext(), R.dimen.spacer_1x), 0);
- setupPeople();
+ setupAssignees();
setupLabels();
setupDueDate();
setupDescription();
+ setupProjects();
binding.description.setText(viewModel.getFullCard().getCard().getDescription());
return binding.getRoot();
}
-
@Override
public void onResume() {
super.onResume();
@@ -173,49 +169,14 @@ public class CardDetailsFragment extends BrandedFragment implements OnDateSetLis
}
}
- private TimePickerDialog createTimePickerDialogFromDate(
- @Nullable OnTimeSetListener listener,
- @Nullable Date date
- ) {
- int hourOfDay = 0;
- int minutes = 0;
-
- if (date != null) {
- hourOfDay = date.getHours();
- minutes = date.getMinutes();
- }
- return BrandedTimePickerDialog.newInstance(listener, hourOfDay, minutes, true);
- }
-
- private DatePickerDialog createDatePickerDialogFromDate(
- @Nullable OnDateSetListener listener,
- @Nullable Date date
- ) {
- int year;
- int month;
- int day;
-
- Calendar cal = Calendar.getInstance();
- if (date != null) {
- cal.setTime(date);
- year = cal.get(Calendar.YEAR);
- month = cal.get(Calendar.MONTH);
- day = cal.get(Calendar.DAY_OF_MONTH);
- } else {
- year = cal.get(Calendar.YEAR);
- month = cal.get(Calendar.MONTH);
- day = cal.get(Calendar.DAY_OF_MONTH);
- }
- return BrandedDatePickerDialog.newInstance(listener, year, month, day);
- }
-
private void setupDueDate() {
if (this.viewModel.getFullCard().getCard().getDueDate() != null) {
- binding.dueDateDate.setText(dateFormat.format(this.viewModel.getFullCard().getCard().getDueDate()));
- binding.dueDateTime.setText(dueTime.format(this.viewModel.getFullCard().getCard().getDueDate()));
- binding.clearDueDate.setVisibility(View.VISIBLE);
+ final ZonedDateTime dueDate = this.viewModel.getFullCard().getCard().getDueDate().atZone(ZoneId.systemDefault());
+ binding.dueDateDate.setText(dueDate == null ? null : dueDate.format(dateFormatter));
+ binding.dueDateTime.setText(dueDate == null ? null : dueDate.format(timeFormatter));
+ binding.clearDueDate.setVisibility(VISIBLE);
} else {
- binding.clearDueDate.setVisibility(View.GONE);
+ binding.clearDueDate.setVisibility(GONE);
binding.dueDateDate.setText(null);
binding.dueDateTime.setText(null);
}
@@ -223,31 +184,37 @@ public class CardDetailsFragment extends BrandedFragment implements OnDateSetLis
if (viewModel.canEdit()) {
binding.dueDateDate.setOnClickListener(v -> {
- if (viewModel.getFullCard() != null && viewModel.getFullCard().getCard() != null) {
- createDatePickerDialogFromDate(this, viewModel.getFullCard().getCard().getDueDate()).show(getChildFragmentManager(), BrandedDatePickerDialog.class.getCanonicalName());
+ final LocalDate date;
+ if (viewModel.getFullCard() != null && viewModel.getFullCard().getCard() != null && viewModel.getFullCard().getCard().getDueDate() != null) {
+ date = viewModel.getFullCard().getCard().getDueDate().atZone(ZoneId.systemDefault()).toLocalDate();
} else {
- createDatePickerDialogFromDate(this, null).show(getChildFragmentManager(), BrandedDatePickerDialog.class.getCanonicalName());
+ date = LocalDate.now();
}
+ BrandedDatePickerDialog.newInstance(this, date.getYear(), date.getMonthValue(), date.getDayOfMonth())
+ .show(getChildFragmentManager(), BrandedDatePickerDialog.class.getCanonicalName());
});
binding.dueDateTime.setOnClickListener(v -> {
- if (viewModel.getFullCard() != null && viewModel.getFullCard().getCard() != null) {
- createTimePickerDialogFromDate(this, viewModel.getFullCard().getCard().getDueDate()).show(getChildFragmentManager(), BrandedTimePickerDialog.class.getCanonicalName());
+ final LocalTime time;
+ if (viewModel.getFullCard() != null && viewModel.getFullCard().getCard() != null && viewModel.getFullCard().getCard().getDueDate() != null) {
+ time = viewModel.getFullCard().getCard().getDueDate().atZone(ZoneId.systemDefault()).toLocalTime();
} else {
- createTimePickerDialogFromDate(this, null).show(getChildFragmentManager(), BrandedTimePickerDialog.class.getCanonicalName());
+ time = LocalTime.now();
}
+ BrandedTimePickerDialog.newInstance(this, time.getHour(), time.getMinute(), true)
+ .show(getChildFragmentManager(), BrandedTimePickerDialog.class.getCanonicalName());
});
binding.clearDueDate.setOnClickListener(v -> {
binding.dueDateDate.setText(null);
binding.dueDateTime.setText(null);
viewModel.getFullCard().getCard().setDueDate(null);
- binding.clearDueDate.setVisibility(View.GONE);
+ binding.clearDueDate.setVisibility(GONE);
});
} else {
binding.dueDateDate.setEnabled(false);
binding.dueDateTime.setEnabled(false);
- binding.clearDueDate.setVisibility(View.GONE);
+ binding.clearDueDate.setVisibility(GONE);
}
}
@@ -266,7 +233,7 @@ public class CardDetailsFragment extends BrandedFragment implements OnDateSetLis
newLabel.setBoardId(boardId);
newLabel.setTitle(((LabelAutoCompleteAdapter) binding.labels.getAdapter()).getLastFilterText());
newLabel.setLocalId(null);
- WrappedLiveData<Label> createLabelLiveData = syncManager.createLabel(accountId, newLabel, boardId);
+ WrappedLiveData<Label> createLabelLiveData = viewModel.createLabel(accountId, newLabel, boardId);
observeOnce(createLabelLiveData, CardDetailsFragment.this, createdLabel -> {
if (createLabelLiveData.hasError()) {
DeckLog.logError(createLabelLiveData.getError());
@@ -277,14 +244,14 @@ public class CardDetailsFragment extends BrandedFragment implements OnDateSetLis
((LabelAutoCompleteAdapter) binding.labels.getAdapter()).exclude(createdLabel);
viewModel.getFullCard().getLabels().add(createdLabel);
binding.labelsGroup.addView(createChipFromLabel(newLabel));
- binding.labelsGroup.setVisibility(View.VISIBLE);
+ binding.labelsGroup.setVisibility(VISIBLE);
}
});
} else {
((LabelAutoCompleteAdapter) binding.labels.getAdapter()).exclude(label);
viewModel.getFullCard().getLabels().add(label);
binding.labelsGroup.addView(createChipFromLabel(label));
- binding.labelsGroup.setVisibility(View.VISIBLE);
+ binding.labelsGroup.setVisibility(VISIBLE);
}
binding.labels.setText("");
@@ -296,18 +263,17 @@ public class CardDetailsFragment extends BrandedFragment implements OnDateSetLis
for (Label label : viewModel.getFullCard().getLabels()) {
binding.labelsGroup.addView(createChipFromLabel(label));
}
- binding.labelsGroup.setVisibility(View.VISIBLE);
+ binding.labelsGroup.setVisibility(VISIBLE);
} else {
binding.labelsGroup.setVisibility(View.INVISIBLE);
}
}
-
private Chip createChipFromLabel(Label label) {
final Chip chip = new Chip(activity);
chip.setText(label.getTitle());
if (viewModel.canEdit()) {
- chip.setCloseIcon(getResources().getDrawable(R.drawable.ic_close_circle_grey600));
+ chip.setCloseIcon(ContextCompat.getDrawable(requireContext(), R.drawable.ic_close_circle_grey600));
chip.setCloseIconVisible(true);
chip.setOnCloseIconClickListener(v -> {
binding.labelsGroup.removeView(chip);
@@ -316,9 +282,9 @@ public class CardDetailsFragment extends BrandedFragment implements OnDateSetLis
});
}
try {
- final int labelColor = Color.parseColor("#" + label.getColor());
+ final int labelColor = label.getColor();
chip.setChipBackgroundColor(ColorStateList.valueOf(labelColor));
- final int color = ColorUtil.getForegroundColorForBackgroundColor(labelColor);
+ final int color = ColorUtil.INSTANCE.getForegroundColorForBackgroundColor(labelColor);
chip.setTextColor(color);
if (chip.getCloseIcon() != null) {
@@ -331,7 +297,15 @@ public class CardDetailsFragment extends BrandedFragment implements OnDateSetLis
return chip;
}
- private void setupPeople() {
+ private void setupAssignees() {
+ adapter = new AssigneeAdapter((user) -> CardAssigneeDialog.newInstance(user).show(getChildFragmentManager(), CardAssigneeDialog.class.getSimpleName()), viewModel.getAccount());
+ binding.assignees.setAdapter(adapter);
+ binding.assignees.post(() -> {
+ @Px final int gutter = DimensionUtil.INSTANCE.dpToPx(requireContext(), R.dimen.spacer_1x);
+ final int spanCount = (int) (float) binding.assignees.getWidth() / (DimensionUtil.INSTANCE.dpToPx(requireContext(), R.dimen.avatar_size) + gutter);
+ binding.assignees.setLayoutManager(new GridLayoutManager(getContext(), spanCount));
+ binding.assignees.addItemDecoration(new AssigneeDecoration(gutter));
+ });
if (viewModel.canEdit()) {
Long localCardId = viewModel.getFullCard().getCard().getLocalId();
localCardId = localCardId == null ? -1 : localCardId;
@@ -340,81 +314,90 @@ public class CardDetailsFragment extends BrandedFragment implements OnDateSetLis
User user = (User) adapterView.getItemAtPosition(position);
viewModel.getFullCard().getAssignedUsers().add(user);
((UserAutoCompleteAdapter) binding.people.getAdapter()).exclude(user);
- addAvatar(viewModel.getAccount().getUrl(), user);
+ adapter.addUser(user);
binding.people.setText("");
});
if (this.viewModel.getFullCard().getAssignedUsers() != null) {
- binding.peopleList.removeAllViews();
- for (User user : this.viewModel.getFullCard().getAssignedUsers()) {
- addAvatar(viewModel.getAccount().getUrl(), user);
- }
+ adapter.setUsers(this.viewModel.getFullCard().getAssignedUsers());
}
} else {
binding.people.setEnabled(false);
}
}
- private void addAvatar(String baseUrl, User user) {
- ImageView avatar = new ImageView(activity);
- avatar.setLayoutParams(avatarLayoutParams);
- if (viewModel.canEdit()) {
- avatar.setOnClickListener(v -> {
- viewModel.getFullCard().getAssignedUsers().remove(user);
- binding.peopleList.removeView(avatar);
- ((UserAutoCompleteAdapter) binding.people.getAdapter()).include(user);
- BrandedSnackbar.make(
- requireView(), getString(R.string.unassigned_user, user.getDisplayname()),
- Snackbar.LENGTH_LONG)
- .setAction(R.string.simple_undo, v1 -> {
- viewModel.getFullCard().getAssignedUsers().add(user);
- ((UserAutoCompleteAdapter) binding.people.getAdapter()).exclude(user);
- addAvatar(baseUrl, user);
- }).show();
- });
- }
- binding.peopleList.addView(avatar);
- avatar.requestLayout();
- ViewUtil.addAvatar(avatar, baseUrl, user.getUid(), avatarSize, R.drawable.ic_person_grey600_24dp);
- }
-
@Override
- public void onDateSet(com.wdullaer.materialdatetimepicker.date.DatePickerDialog view, int year, int monthOfYear, int dayOfMonth) {
- Calendar c = Calendar.getInstance();
+ public void onDateSet(DatePickerDialog view, int year, int monthOfYear, int dayOfMonth) {
int hourOfDay;
int minute;
- if (binding.dueDateTime.getText() != null && binding.dueDateTime.length() > 0) {
- hourOfDay = this.viewModel.getFullCard().getCard().getDueDate().getHours();
- minute = this.viewModel.getFullCard().getCard().getDueDate().getMinutes();
- } else {
+ final CharSequence selectedTime = binding.dueDateTime.getText();
+ if (TextUtils.isEmpty(selectedTime)) {
hourOfDay = 0;
minute = 0;
+ } else {
+ final LocalTime oldTime = LocalTime.from(this.viewModel.getFullCard().getCard().getDueDate().atZone(ZoneId.systemDefault()));
+ hourOfDay = oldTime.getHour();
+ minute = oldTime.getMinute();
}
- c.set(year, monthOfYear, dayOfMonth, hourOfDay, minute);
- this.viewModel.getFullCard().getCard().setDueDate(c.getTime());
- binding.dueDateDate.setText(dateFormat.format(c.getTime()));
+ final ZonedDateTime newDateTime = ZonedDateTime.of(
+ LocalDate.of(year, monthOfYear + 1, dayOfMonth),
+ LocalTime.of(hourOfDay, minute),
+ ZoneId.systemDefault()
+ );
+ this.viewModel.getFullCard().getCard().setDueDate(newDateTime.toInstant());
+ binding.dueDateDate.setText(newDateTime.format(dateFormatter));
- if (this.viewModel.getFullCard().getCard().getDueDate() == null || this.viewModel.getFullCard().getCard().getDueDate().getTime() == 0) {
- binding.clearDueDate.setVisibility(View.GONE);
+ if (this.viewModel.getFullCard().getCard().getDueDate() == null || this.viewModel.getFullCard().getCard().getDueDate().toEpochMilli() == 0) {
+ binding.clearDueDate.setVisibility(GONE);
} else {
- binding.clearDueDate.setVisibility(View.VISIBLE);
+ binding.clearDueDate.setVisibility(VISIBLE);
}
}
@Override
- public void onTimeSet(com.wdullaer.materialdatetimepicker.time.TimePickerDialog view, int hourOfDay, int minute, int second) {
- if (this.viewModel.getFullCard().getCard().getDueDate() == null) {
- this.viewModel.getFullCard().getCard().setDueDate(new Date());
+ public void onTimeSet(TimePickerDialog view, int hourOfDay, int minute, int second) {
+ final Instant oldInstant = this.viewModel.getFullCard().getCard().getDueDate();
+ final ZonedDateTime oldDateTime = oldInstant == null ? ZonedDateTime.now() : oldInstant.atZone(ZoneId.systemDefault());
+ final ZonedDateTime newDateTime = oldDateTime.with(
+ LocalTime.of(hourOfDay, minute)
+ );
+
+ this.viewModel.getFullCard().getCard().setDueDate(newDateTime.toInstant());
+ binding.dueDateTime.setText(newDateTime.format(timeFormatter));
+ if (this.viewModel.getFullCard().getCard().getDueDate() == null || this.viewModel.getFullCard().getCard().getDueDate().toEpochMilli() == 0) {
+ binding.clearDueDate.setVisibility(GONE);
+ } else {
+ binding.clearDueDate.setVisibility(VISIBLE);
}
- this.viewModel.getFullCard().getCard().getDueDate().setHours(hourOfDay);
- this.viewModel.getFullCard().getCard().getDueDate().setMinutes(minute);
- binding.dueDateTime.setText(dueTime.format(this.viewModel.getFullCard().getCard().getDueDate().getTime()));
- if (this.viewModel.getFullCard().getCard().getDueDate() == null || this.viewModel.getFullCard().getCard().getDueDate().getTime() == 0) {
- binding.clearDueDate.setVisibility(View.GONE);
+ }
+
+ private void setupProjects() {
+ if (viewModel.getFullCard().getProjects().size() > 0) {
+ binding.projectsTitle.setVisibility(VISIBLE);
+ binding.projects.setNestedScrollingEnabled(false);
+ final CardProjectsAdapter adapter = new CardProjectsAdapter(viewModel.getFullCard().getProjects(), getChildFragmentManager());
+ binding.projects.setAdapter(adapter);
+ binding.projects.setVisibility(VISIBLE);
} else {
- binding.clearDueDate.setVisibility(View.VISIBLE);
+ binding.projectsTitle.setVisibility(GONE);
+ binding.projects.setVisibility(GONE);
}
}
+
+ @Override
+ public void onUnassignUser(@NonNull User user) {
+ viewModel.getFullCard().getAssignedUsers().remove(user);
+ adapter.removeUser(user);
+ ((UserAutoCompleteAdapter) binding.people.getAdapter()).include(user);
+ BrandedSnackbar.make(
+ requireView(), getString(R.string.unassigned_user, user.getDisplayname()),
+ Snackbar.LENGTH_LONG)
+ .setAction(R.string.simple_undo, v1 -> {
+ viewModel.getFullCard().getAssignedUsers().add(user);
+ ((UserAutoCompleteAdapter) binding.people.getAdapter()).exclude(user);
+ adapter.addUser(user);
+ }).show();
+ }
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardDetailsListener.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardDetailsListener.java
deleted file mode 100644
index 2efbab789..000000000
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardDetailsListener.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package it.niedermann.nextcloud.deck.ui.card.details;
-
-import java.util.Date;
-
-import it.niedermann.nextcloud.deck.model.Label;
-import it.niedermann.nextcloud.deck.model.User;
-
-public interface CardDetailsListener {
-
- void onDescriptionChanged(String toString);
-
- void onDueDateChanged(Date dueDate);
-
- void onUserAdded(User user);
-
- void onUserRemoved(User user);
-
- void onLabelRemoved(Label label);
-
- void onLabelAdded(Label createdLabel);
-} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardProjectsAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardProjectsAdapter.java
new file mode 100644
index 000000000..0c2d63d74
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardProjectsAdapter.java
@@ -0,0 +1,52 @@
+package it.niedermann.nextcloud.deck.ui.card.details;
+
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.fragment.app.FragmentManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import it.niedermann.nextcloud.deck.databinding.ItemProjectBinding;
+import it.niedermann.nextcloud.deck.model.ocs.projects.full.OcsProjectWithResources;
+import it.niedermann.nextcloud.deck.ui.card.projectresources.CardProjectResourcesDialog;
+
+public class CardProjectsAdapter extends RecyclerView.Adapter<CardProjectsViewHolder> {
+
+ @NonNull
+ private final List<OcsProjectWithResources> projects;
+ @NonNull
+ private final FragmentManager fragmentManager;
+
+ public CardProjectsAdapter(@NonNull List<OcsProjectWithResources> projects, @NonNull FragmentManager fragmentManager) {
+ this.projects = new ArrayList<>(projects.size());
+ this.projects.addAll(projects);
+ this.fragmentManager = fragmentManager;
+ setHasStableIds(true);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return projects.get(position).getLocalId();
+ }
+
+ @NonNull
+ @Override
+ public CardProjectsViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ return new CardProjectsViewHolder(ItemProjectBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false));
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull CardProjectsViewHolder holder, int position) {
+ final OcsProjectWithResources project = projects.get(position);
+ holder.bind(project, (v) -> CardProjectResourcesDialog.newInstance(project.getName(), project.getResources()).show(fragmentManager, CardProjectResourcesDialog.class.getSimpleName()));
+ }
+
+ @Override
+ public int getItemCount() {
+ return projects.size();
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardProjectsViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardProjectsViewHolder.java
new file mode 100644
index 000000000..9c5494af7
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardProjectsViewHolder.java
@@ -0,0 +1,32 @@
+package it.niedermann.nextcloud.deck.ui.card.details;
+
+import android.view.View.OnClickListener;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.RecyclerView;
+
+import it.niedermann.nextcloud.deck.R;
+import it.niedermann.nextcloud.deck.databinding.ItemProjectBinding;
+import it.niedermann.nextcloud.deck.model.ocs.projects.full.OcsProjectWithResources;
+
+public class CardProjectsViewHolder extends RecyclerView.ViewHolder {
+
+ private ItemProjectBinding binding;
+
+ public CardProjectsViewHolder(@NonNull ItemProjectBinding binding) {
+ super(binding.getRoot());
+ this.binding = binding;
+ }
+
+ public void bind(@NonNull OcsProjectWithResources project, @Nullable OnClickListener onClickListener) {
+ binding.projectName.setText(project.getName());
+ final int resourcesCount = project.getResources().size();
+ binding.resourcesCount.setText(itemView.getContext().getResources().getQuantityString(R.plurals.resources_count, resourcesCount, resourcesCount));
+ if (resourcesCount > 0) {
+ binding.getRoot().setOnClickListener(onClickListener);
+ } else {
+ binding.getRoot().setOnClickListener(null);
+ }
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/projectresources/CardProjectResourceAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/projectresources/CardProjectResourceAdapter.java
new file mode 100644
index 000000000..4c95574c3
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/projectresources/CardProjectResourceAdapter.java
@@ -0,0 +1,54 @@
+package it.niedermann.nextcloud.deck.ui.card.projectresources;
+
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import it.niedermann.nextcloud.deck.databinding.ItemProjectResourceBinding;
+import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProjectResource;
+import it.niedermann.nextcloud.deck.ui.card.EditCardViewModel;
+
+public class CardProjectResourceAdapter extends RecyclerView.Adapter<CardProjectResourceViewHolder> {
+
+ @NonNull
+ private final EditCardViewModel viewModel;
+ @NonNull
+ private final List<OcsProjectResource> resources;
+ @NonNull
+ private final LifecycleOwner owner;
+
+ public CardProjectResourceAdapter(@NonNull EditCardViewModel viewModel, @NonNull List<OcsProjectResource> resources, @NonNull LifecycleOwner owner) {
+ this.viewModel = viewModel;
+ this.resources = new ArrayList<>(resources.size());
+ this.resources.addAll(resources);
+ this.owner = owner;
+ setHasStableIds(true);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return resources.get(position).getLocalId();
+ }
+
+ @NonNull
+ @Override
+ public CardProjectResourceViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ return new CardProjectResourceViewHolder(ItemProjectResourceBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false));
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull CardProjectResourceViewHolder holder, int position) {
+ holder.bind(viewModel, resources.get(position), owner);
+ }
+
+ @Override
+ public int getItemCount() {
+ return this.resources.size();
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/projectresources/CardProjectResourceViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/projectresources/CardProjectResourceViewHolder.java
new file mode 100644
index 000000000..272945e45
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/projectresources/CardProjectResourceViewHolder.java
@@ -0,0 +1,110 @@
+package it.niedermann.nextcloud.deck.ui.card.projectresources;
+
+import android.content.Intent;
+import android.content.res.Resources;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.recyclerview.widget.RecyclerView;
+
+import it.niedermann.nextcloud.deck.DeckLog;
+import it.niedermann.nextcloud.deck.R;
+import it.niedermann.nextcloud.deck.databinding.ItemProjectResourceBinding;
+import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProjectResource;
+import it.niedermann.nextcloud.deck.ui.card.EditActivity;
+import it.niedermann.nextcloud.deck.ui.card.EditCardViewModel;
+import it.niedermann.nextcloud.deck.util.ProjectUtil;
+
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+import static it.niedermann.nextcloud.deck.util.ProjectUtil.getResourceUri;
+
+public class CardProjectResourceViewHolder extends RecyclerView.ViewHolder {
+ @NonNull
+ private final ItemProjectResourceBinding binding;
+
+ public CardProjectResourceViewHolder(@NonNull ItemProjectResourceBinding binding) {
+ super(binding.getRoot());
+ this.binding = binding;
+ }
+
+ public void bind(@NonNull EditCardViewModel viewModel, @NonNull OcsProjectResource resource, @NonNull LifecycleOwner owner) {
+ final Account account = viewModel.getAccount();
+ final Resources resources = itemView.getResources();
+ binding.name.setText(resource.getName());
+ final @Nullable String link = resource.getLink();
+ binding.type.setVisibility(VISIBLE);
+ if (resource.getType() != null) {
+ switch (resource.getType()) {
+ case "deck": {
+ // TODO https://github.com/stefan-niedermann/nextcloud-deck/issues/671
+ linkifyViewHolder(account, link);
+ binding.type.setText(resources.getString(R.string.project_type_deck_board));
+ binding.image.setImageResource(R.drawable.project_deck_36dp);
+ break;
+ }
+ case "deck-card": {
+ try {
+ long[] ids = ProjectUtil.extractBoardIdAndCardIdFromUrl(link);
+ if (ids.length == 2) {
+ viewModel.getCardByRemoteID(account.getId(), ids[1]).observe(owner, (fullCard) -> {
+ if (fullCard != null) {
+ viewModel.getBoardByRemoteId(account.getId(), ids[0]).observe(owner, (board) -> {
+ if (board != null) {
+ binding.getRoot().setOnClickListener((v) -> itemView.getContext().startActivity(EditActivity.createEditCardIntent(itemView.getContext(), account, board.getLocalId(), fullCard.getLocalId())));
+ } else {
+ linkifyViewHolder(account, link);
+ }
+ });
+ } else {
+ linkifyViewHolder(account, link);
+ }
+ });
+ } else {
+ linkifyViewHolder(account, link);
+ }
+ } catch (IllegalArgumentException e) {
+ DeckLog.logError(e);
+ linkifyViewHolder(account, link);
+ }
+ binding.type.setText(resources.getString(R.string.project_type_deck_card));
+ binding.image.setImageResource(R.drawable.project_deck_36dp);
+ break;
+ }
+ case "file": {
+ binding.type.setText(resources.getString(R.string.project_type_file));
+ linkifyViewHolder(account, link);
+ binding.image.setImageResource(R.drawable.project_file_36dp);
+ break;
+ }
+ case "room": {
+ binding.type.setText(resources.getString(R.string.project_type_room));
+ linkifyViewHolder(account, link);
+ binding.image.setImageResource(R.drawable.project_talk_36dp);
+ break;
+ }
+ default: {
+ DeckLog.info("Unknown resource type for " + resource.getName() + ": " + resource.getType());
+ binding.type.setVisibility(GONE);
+ linkifyViewHolder(account, link);
+ break;
+ }
+ }
+ } else {
+ DeckLog.warn("Resource type for " + resource.getName() + " is null");
+ binding.type.setVisibility(GONE);
+ }
+ }
+
+ private void linkifyViewHolder(@NonNull Account account, @Nullable String link) {
+ if (link != null) {
+ try {
+ binding.getRoot().setOnClickListener((v) -> itemView.getContext().startActivity(new Intent(Intent.ACTION_VIEW).setData(getResourceUri(account, link))));
+ } catch (IllegalArgumentException e) {
+ DeckLog.logError(e);
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/projectresources/CardProjectResourcesDialog.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/projectresources/CardProjectResourcesDialog.java
new file mode 100644
index 000000000..46195b309
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/projectresources/CardProjectResourcesDialog.java
@@ -0,0 +1,83 @@
+package it.niedermann.nextcloud.deck.ui.card.projectresources;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.DialogFragment;
+import androidx.lifecycle.ViewModelProvider;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+import it.niedermann.nextcloud.deck.R;
+import it.niedermann.nextcloud.deck.databinding.DialogProjectResourcesBinding;
+import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProjectResource;
+import it.niedermann.nextcloud.deck.ui.branding.BrandedAlertDialogBuilder;
+import it.niedermann.nextcloud.deck.ui.branding.BrandedDialogFragment;
+import it.niedermann.nextcloud.deck.ui.card.EditCardViewModel;
+
+public class CardProjectResourcesDialog extends BrandedDialogFragment {
+
+ private static final String KEY_RESOURCES = "resources";
+ private static final String KEY_PROJECT_NAME = "projectName";
+ private DialogProjectResourcesBinding binding;
+ private EditCardViewModel viewModel;
+
+ private String projectName;
+ @NonNull
+ private List<OcsProjectResource> resources = new ArrayList<>();
+
+ @Override
+ public void onAttach(@NonNull Context context) {
+ super.onAttach(context);
+ final Bundle args = requireArguments();
+ if (!args.containsKey(KEY_RESOURCES)) {
+ throw new IllegalArgumentException("Provide at least " + KEY_RESOURCES);
+ }
+ //noinspection unchecked
+ this.resources.addAll((ArrayList<OcsProjectResource>) Objects.requireNonNull(args.getSerializable(KEY_RESOURCES)));
+ this.projectName = args.getString(KEY_PROJECT_NAME);
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ binding = DialogProjectResourcesBinding.inflate(LayoutInflater.from(requireContext()));
+ viewModel = new ViewModelProvider(requireActivity()).get(EditCardViewModel.class);
+
+ AlertDialog.Builder dialogBuilder = new BrandedAlertDialogBuilder(requireContext());
+
+ return dialogBuilder
+ .setTitle(projectName)
+ .setView(binding.getRoot())
+ .setNeutralButton(R.string.simple_close, null)
+ .create();
+ }
+
+ @Override
+ public void onActivityCreated(@Nullable Bundle savedInstanceState) {
+ final CardProjectResourceAdapter adapter = new CardProjectResourceAdapter(viewModel, resources, requireActivity());
+ binding.getRoot().setAdapter(adapter);
+ super.onActivityCreated(savedInstanceState);
+ }
+
+ @Override
+ public void applyBrand(int mainColor) {
+
+ }
+
+ public static DialogFragment newInstance(@Nullable String projectName, @NonNull List<OcsProjectResource> resources) {
+ final DialogFragment fragment = new CardProjectResourcesDialog();
+ final Bundle args = new Bundle();
+ args.putString(KEY_PROJECT_NAME, projectName);
+ args.putSerializable(KEY_RESOURCES, new ArrayList<>(resources));
+ fragment.setArguments(args);
+ return fragment;
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionActivity.java
index 9eef878c3..ac6335b90 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionActivity.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionActivity.java
@@ -8,13 +8,12 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
-import it.niedermann.nextcloud.deck.DeckLog;
+import it.niedermann.android.util.ClipboardUtil;
+import it.niedermann.nextcloud.deck.BuildConfig;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.ActivityExceptionBinding;
import it.niedermann.nextcloud.deck.ui.exception.tips.TipsAdapter;
-import it.niedermann.nextcloud.deck.util.ExceptionUtil;
-
-import static it.niedermann.nextcloud.deck.util.ClipboardUtil.copyToClipboard;
+import it.niedermann.nextcloud.exception.ExceptionUtil;
public class ExceptionActivity extends AppCompatActivity {
@@ -22,9 +21,12 @@ public class ExceptionActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
final ActivityExceptionBinding binding = ActivityExceptionBinding.inflate(getLayoutInflater());
+
setContentView(binding.getRoot());
- super.onCreate(savedInstanceState);
+ setSupportActionBar(binding.toolbar);
Throwable throwable = ((Throwable) getIntent().getSerializableExtra(KEY_THROWABLE));
@@ -32,23 +34,18 @@ public class ExceptionActivity extends AppCompatActivity {
throwable = new Exception("Could not get exception");
}
- DeckLog.logError(throwable);
+ final TipsAdapter adapter = new TipsAdapter(this::startActivity);
+ final String debugInfo = "Full Crash:\n\n" + ExceptionUtil.INSTANCE.getDebugInfos(this, throwable, BuildConfig.FLAVOR);
- setSupportActionBar(binding.toolbar);
+ binding.tips.setAdapter(adapter);
+ binding.tips.setNestedScrollingEnabled(false);
binding.toolbar.setTitle(R.string.error);
binding.message.setText(throwable.getMessage());
-
- final String debugInfo = ExceptionUtil.getDebugInfos(this, throwable, null);
-
binding.stacktrace.setText(debugInfo);
+ binding.copy.setOnClickListener((v) -> ClipboardUtil.INSTANCE.copyToClipboard(this, getString(R.string.simple_exception), "```\n" + debugInfo + "\n```"));
+ binding.close.setOnClickListener((v) -> finish());
- final TipsAdapter adapter = new TipsAdapter(this::startActivity);
- binding.tips.setAdapter(adapter);
- binding.tips.setNestedScrollingEnabled(false);
adapter.setThrowable(this, null, throwable);
-
- binding.copy.setOnClickListener((v) -> copyToClipboard(this, getString(R.string.simple_exception), "```\n" + debugInfo + "\n```"));
- binding.close.setOnClickListener((v) -> finish());
}
@NonNull
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionDialogFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionDialogFragment.java
index 6c0d0ba79..7a84ce0b6 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionDialogFragment.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionDialogFragment.java
@@ -11,14 +11,14 @@ import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatDialogFragment;
import androidx.fragment.app.DialogFragment;
+import it.niedermann.android.util.ClipboardUtil;
+import it.niedermann.nextcloud.deck.BuildConfig;
import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.DialogExceptionBinding;
import it.niedermann.nextcloud.deck.model.Account;
import it.niedermann.nextcloud.deck.ui.exception.tips.TipsAdapter;
-import it.niedermann.nextcloud.deck.util.ExceptionUtil;
-
-import static it.niedermann.nextcloud.deck.util.ClipboardUtil.copyToClipboard;
+import it.niedermann.nextcloud.exception.ExceptionUtil;
public class ExceptionDialogFragment extends AppCompatDialogFragment {
@@ -52,7 +52,7 @@ public class ExceptionDialogFragment extends AppCompatDialogFragment {
final TipsAdapter adapter = new TipsAdapter((actionIntent) -> requireActivity().startActivity(actionIntent));
- final String debugInfos = ExceptionUtil.getDebugInfos(requireContext(), throwable, account);
+ final String debugInfos = ExceptionUtil.INSTANCE.getDebugInfos(requireContext(), throwable, BuildConfig.FLAVOR, account == null ? null : account.getServerDeckVersion());
binding.tips.setAdapter(adapter);
binding.stacktrace.setText(debugInfos);
@@ -65,7 +65,7 @@ public class ExceptionDialogFragment extends AppCompatDialogFragment {
.setView(binding.getRoot())
.setTitle(R.string.error_dialog_title)
.setPositiveButton(android.R.string.copy, (a, b) -> {
- copyToClipboard(requireContext(), getString(R.string.simple_exception), "```\n" + debugInfos + "\n```");
+ ClipboardUtil.INSTANCE.copyToClipboard(requireContext(), getString(R.string.simple_exception), "```\n" + debugInfos + "\n```");
a.dismiss();
})
.setNegativeButton(R.string.simple_close, null)
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionHandler.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionHandler.java
index 8f0bfce33..c62b23e51 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionHandler.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionHandler.java
@@ -2,21 +2,24 @@ package it.niedermann.nextcloud.deck.ui.exception;
import android.app.Activity;
-import org.jetbrains.annotations.NotNull;
+import androidx.annotation.NonNull;
+
+import it.niedermann.nextcloud.deck.DeckLog;
public class ExceptionHandler implements Thread.UncaughtExceptionHandler {
- private Activity context;
+ @NonNull
+ private final Activity activity;
- public ExceptionHandler(Activity context) {
- super();
- this.context = context;
+ public ExceptionHandler(@NonNull Activity activity) {
+ this.activity = activity;
}
@Override
- public void uncaughtException(@NotNull Thread t, Throwable e) {
- context.getApplicationContext().startActivity(ExceptionActivity.createIntent(context, e));
- context.finish();
+ public void uncaughtException(@NonNull Thread t, @NonNull Throwable e) {
+ DeckLog.logError(e);
+ activity.getApplicationContext().startActivity(ExceptionActivity.createIntent(activity, e));
+ activity.finish();
Runtime.getRuntime().exit(0);
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/tips/TipsAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/tips/TipsAdapter.java
index a059b2956..6bfd82b13 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/tips/TipsAdapter.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/tips/TipsAdapter.java
@@ -39,6 +39,10 @@ import static it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment.
public class TipsAdapter extends RecyclerView.Adapter<TipsViewHolder> {
+ private static final Intent INTENT_APP_INFO = new Intent(ACTION_APPLICATION_DETAILS_SETTINGS)
+ .setData(Uri.parse("package:" + BuildConfig.APPLICATION_ID))
+ .putExtra(INTENT_EXTRA_BUTTON_TEXT, R.string.error_action_open_deck_info);
+
@NonNull
private Consumer<Intent> actionButtonClickedListener;
@NonNull
@@ -68,11 +72,8 @@ public class TipsAdapter extends RecyclerView.Adapter<TipsViewHolder> {
public void setThrowable(@NonNull Context context, @Nullable Account account, @NonNull Throwable throwable) {
if (throwable instanceof TokenMismatchException) {
add(R.string.error_dialog_tip_token_mismatch_retry);
- add(R.string.error_dialog_tip_token_mismatch_clear_storage);
- Intent intent = new Intent(ACTION_APPLICATION_DETAILS_SETTINGS)
- .setData(Uri.parse("package:" + BuildConfig.APPLICATION_ID))
- .putExtra(INTENT_EXTRA_BUTTON_TEXT, R.string.error_action_open_deck_info);
- add(R.string.error_dialog_tip_clear_storage, intent);
+ add(R.string.error_dialog_tip_clear_storage_might_help);
+ add(R.string.error_dialog_tip_clear_storage, INTENT_APP_INFO);
} else if (throwable instanceof NextcloudFilesAppNotSupportedException) {
add(R.string.error_dialog_tip_files_outdated);
} else if (throwable instanceof NextcloudApiNotRespondingException) {
@@ -122,6 +123,10 @@ public class TipsAdapter extends RecyclerView.Adapter<TipsViewHolder> {
} else {
add(R.string.error_dialog_version_not_parsable);
}
+ add(R.string.error_dialog_account_might_not_be_authorized);
+ break;
+ case UNKNOWN_ACCOUNT_USER_ID:
+ add(R.string.error_dialog_user_not_found_in_database);
break;
case CAPABILITIES_NOT_PARSABLE:
default:
@@ -133,15 +138,15 @@ public class TipsAdapter extends RecyclerView.Adapter<TipsViewHolder> {
add(R.string.error_dialog_capabilities_not_parsable);
}
}
+ // Files app might no longer be authenticated: https://github.com/stefan-niedermann/nextcloud-deck/issues/621#issuecomment-665533567
+ add(R.string.error_dialog_tip_clear_storage_might_help);
+ add(R.string.error_dialog_tip_clear_storage, INTENT_APP_INFO);
} else if (throwable instanceof RuntimeException) {
if (throwable.getMessage() != null && throwable.getMessage().contains("database")) {
Intent reportIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(context.getString(R.string.url_report_bug)))
.putExtra(INTENT_EXTRA_BUTTON_TEXT, R.string.error_action_report_issue);
add(R.string.error_dialog_tip_database_upgrade_failed, reportIntent);
- Intent clearIntent = new Intent(ACTION_APPLICATION_DETAILS_SETTINGS)
- .setData(Uri.parse("package:" + BuildConfig.APPLICATION_ID))
- .putExtra(INTENT_EXTRA_BUTTON_TEXT, R.string.error_action_open_deck_info);
- add(R.string.error_dialog_tip_clear_storage, clearIntent);
+ add(R.string.error_dialog_tip_clear_storage, INTENT_APP_INFO);
}
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterDialogFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterDialogFragment.java
index aa6f59d04..6aa03b811 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterDialogFragment.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterDialogFragment.java
@@ -8,6 +8,7 @@ import android.os.Bundle;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
+import androidx.core.content.ContextCompat;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
@@ -19,6 +20,7 @@ import androidx.viewpager2.widget.ViewPager2;
import com.google.android.material.tabs.TabLayoutMediator;
+import it.niedermann.android.util.ColorUtil;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.DialogFilterBinding;
import it.niedermann.nextcloud.deck.model.enums.EDueType;
@@ -45,8 +47,9 @@ public class FilterDialogFragment extends BrandedDialogFragment {
public Dialog onCreateDialog(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- indicator = getResources().getDrawable(R.drawable.circle_grey600_8dp);
- indicator.setColorFilter(getResources().getColor(R.color.primary), PorterDuff.Mode.SRC_ATOP);
+ indicator = ContextCompat.getDrawable(requireContext(), R.drawable.circle_grey600_8dp);
+ assert indicator != null;
+ indicator.setColorFilter(getResources().getColor(R.color.defaultBrand), PorterDuff.Mode.SRC_ATOP);
filterViewModel = new ViewModelProvider(requireActivity()).get(FilterViewModel.class);
@@ -61,10 +64,10 @@ public class FilterDialogFragment extends BrandedDialogFragment {
filterInformationDraft.observe(this, (draft) -> {
switch (position) {
case 0:
- tab.setIcon(draft.getLabels().size() > 0 ? indicator : null);
+ tab.setIcon(draft.getLabels().size() > 0 || draft.isNoAssignedLabel() ? indicator : null);
break;
case 1:
- tab.setIcon(draft.getUsers().size() > 0 ? indicator : null);
+ tab.setIcon(draft.getUsers().size() > 0 || draft.isNoAssignedUser() ? indicator : null);
break;
case 2:
tab.setIcon(draft.getDueType() != EDueType.NO_FILTER ? indicator : null);
@@ -103,9 +106,10 @@ public class FilterDialogFragment extends BrandedDialogFragment {
@Override
public void applyBrand(int mainColor) {
- @ColorInt int finalMainColor = getSecondaryForegroundColorDependingOnTheme(requireContext(), mainColor);
- binding.tabLayout.setSelectedTabIndicatorColor(finalMainColor);
- indicator.setColorFilter(finalMainColor, PorterDuff.Mode.SRC_ATOP);
+ @ColorInt final int finalMainColor = getSecondaryForegroundColorDependingOnTheme(binding.tabLayout.getContext(), mainColor);
+ final boolean contrastRatioIsSufficient = ColorUtil.INSTANCE.getContrastRatio(mainColor, ContextCompat.getColor(binding.tabLayout.getContext(), R.color.primary)) > 1.7d;
+ binding.tabLayout.setSelectedTabIndicatorColor(contrastRatioIsSufficient ? mainColor : finalMainColor);
+ indicator.setColorFilter(contrastRatioIsSufficient ? mainColor : finalMainColor, PorterDuff.Mode.SRC_ATOP);
}
private static class TabsPagerAdapter extends FragmentStateAdapter {
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterLabelsAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterLabelsAdapter.java
index 096f0db9c..39fb791be 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterLabelsAdapter.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterLabelsAdapter.java
@@ -1,20 +1,21 @@
package it.niedermann.nextcloud.deck.ui.filter;
import android.content.res.ColorStateList;
-import android.graphics.Color;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;
+import it.niedermann.android.util.ColorUtil;
+import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.ItemFilterLabelBinding;
import it.niedermann.nextcloud.deck.model.Label;
-import it.niedermann.nextcloud.deck.util.ColorUtil;
@SuppressWarnings("WeakerAccess")
public class FilterLabelsAdapter extends RecyclerView.Adapter<FilterLabelsAdapter.LabelViewHolder> {
@@ -23,11 +24,17 @@ public class FilterLabelsAdapter extends RecyclerView.Adapter<FilterLabelsAdapte
@NonNull
private final List<Label> selectedLabels = new ArrayList<>();
@Nullable
+ private static final Label NOT_ASSIGNED = null;
+ @Nullable
private final SelectionListener<Label> selectionListener;
- public FilterLabelsAdapter(@NonNull List<Label> labels, @NonNull List<Label> selectedLabels, @Nullable SelectionListener<Label> selectionListener) {
+ public FilterLabelsAdapter(@NonNull List<Label> labels, @NonNull List<Label> selectedLabels, boolean noAssignedLabel, @Nullable SelectionListener<Label> selectionListener) {
super();
+ this.labels.add(NOT_ASSIGNED);
this.labels.addAll(labels);
+ if (noAssignedLabel) {
+ this.selectedLabels.add(NOT_ASSIGNED);
+ }
this.selectedLabels.addAll(selectedLabels);
this.selectionListener = selectionListener;
setHasStableIds(true);
@@ -36,7 +43,8 @@ public class FilterLabelsAdapter extends RecyclerView.Adapter<FilterLabelsAdapte
@Override
public long getItemId(int position) {
- return labels.get(position).getLocalId();
+ @Nullable final Label label = labels.get(position);
+ return label == null ? -1L : label.getLocalId();
}
@NonNull
@@ -47,7 +55,11 @@ public class FilterLabelsAdapter extends RecyclerView.Adapter<FilterLabelsAdapte
@Override
public void onBindViewHolder(@NonNull LabelViewHolder viewHolder, int position) {
- viewHolder.bind(labels.get(position));
+ if (position == 0) {
+ viewHolder.bindNotAssigned();
+ } else {
+ viewHolder.bind(labels.get(position));
+ }
}
@Override
@@ -55,26 +67,36 @@ public class FilterLabelsAdapter extends RecyclerView.Adapter<FilterLabelsAdapte
return labels.size();
}
- public List<Label> getSelected() {
- return selectedLabels;
- }
-
class LabelViewHolder extends RecyclerView.ViewHolder {
private ItemFilterLabelBinding binding;
LabelViewHolder(@NonNull ItemFilterLabelBinding binding) {
super(binding.getRoot());
this.binding = binding;
+ this.binding.label.setClickable(false);
}
void bind(final Label label) {
binding.label.setText(label.getTitle());
- final int labelColor = Color.parseColor("#" + label.getColor());
+ final int labelColor = label.getColor();
binding.label.setChipBackgroundColor(ColorStateList.valueOf(labelColor));
- final int color = ColorUtil.getForegroundColorForBackgroundColor(labelColor);
+ final int color = ColorUtil.INSTANCE.getForegroundColorForBackgroundColor(labelColor);
binding.label.setTextColor(color);
itemView.setSelected(selectedLabels.contains(label));
+ bindClickListener(label);
+ }
+
+ public void bindNotAssigned() {
+ binding.label.setText(itemView.getContext().getString(R.string.no_assigned_label));
+ binding.label.setTextColor(ColorStateList.valueOf(ContextCompat.getColor(itemView.getContext(), R.color.accent)));
+ binding.label.setChipIcon(ContextCompat.getDrawable(itemView.getContext(), R.drawable.ic_baseline_block_24));
+ binding.label.setChipBackgroundColor(ColorStateList.valueOf(ContextCompat.getColor(itemView.getContext(), R.color.primary)));
+ binding.label.setRippleColor(null);
+ itemView.setSelected(selectedLabels.contains(NOT_ASSIGNED));
+ bindClickListener(NOT_ASSIGNED);
+ }
+ private void bindClickListener(@Nullable Label label) {
itemView.setOnClickListener(view -> {
if (selectedLabels.contains(label)) {
selectedLabels.remove(label);
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterLabelsFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterLabelsFragment.java
index 357f93cf9..e7d693185 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterLabelsFragment.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterLabelsFragment.java
@@ -12,7 +12,6 @@ import androidx.lifecycle.ViewModelProvider;
import it.niedermann.nextcloud.deck.databinding.DialogFilterLabelsBinding;
import it.niedermann.nextcloud.deck.model.Label;
-import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
import it.niedermann.nextcloud.deck.ui.MainViewModel;
import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce;
@@ -31,21 +30,33 @@ public class FilterLabelsFragment extends Fragment implements SelectionListener<
filterViewModel = new ViewModelProvider(requireActivity()).get(FilterViewModel.class);
- observeOnce(new SyncManager(requireContext()).findProposalsForLabelsToAssign(mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId()), requireActivity(), (labels) -> {
+ observeOnce(filterViewModel.findProposalsForLabelsToAssign(mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId()), requireActivity(), (labels) -> {
binding.labels.setNestedScrollingEnabled(false);
- binding.labels.setAdapter(new FilterLabelsAdapter(labels, requireNonNull(filterViewModel.getFilterInformationDraft().getValue()).getLabels(), this));
+ binding.labels.setAdapter(new FilterLabelsAdapter(
+ labels,
+ requireNonNull(filterViewModel.getFilterInformationDraft().getValue()).getLabels(),
+ requireNonNull(filterViewModel.getFilterInformationDraft().getValue()).isNoAssignedLabel(),
+ this));
});
return binding.getRoot();
}
@Override
- public void onItemSelected(Label item) {
- filterViewModel.addFilterInformationDraftLabel(item);
+ public void onItemSelected(@Nullable Label item) {
+ if (item == null) {
+ filterViewModel.setNotAssignedLabel(true);
+ } else {
+ filterViewModel.addFilterInformationDraftLabel(item);
+ }
}
@Override
- public void onItemDeselected(Label item) {
- filterViewModel.removeFilterInformationLabel(item);
+ public void onItemDeselected(@Nullable Label item) {
+ if (item == null) {
+ filterViewModel.setNotAssignedLabel(false);
+ } else {
+ filterViewModel.removeFilterInformationLabel(item);
+ }
}
}
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 b4ae8f679..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
@@ -8,6 +8,8 @@ import androidx.annotation.Nullable;
import androidx.annotation.Px;
import androidx.recyclerview.widget.RecyclerView;
+import com.bumptech.glide.Glide;
+
import java.util.ArrayList;
import java.util.List;
@@ -23,6 +25,8 @@ public class FilterUserAdapter extends RecyclerView.Adapter<FilterUserAdapter.Us
final int avatarSize;
@NonNull
private final Account account;
+ @Nullable
+ private static final User NOT_ASSIGNED = null;
@NonNull
private final List<User> users = new ArrayList<>();
@NonNull
@@ -30,11 +34,15 @@ public class FilterUserAdapter extends RecyclerView.Adapter<FilterUserAdapter.Us
@Nullable
private final SelectionListener<User> selectionListener;
- public FilterUserAdapter(@Px int avatarSize, @NonNull Account account, @NonNull List<User> users, @NonNull List<User> selectedUsers, @Nullable SelectionListener selectionListener) {
+ public FilterUserAdapter(@Px int avatarSize, @NonNull Account account, @NonNull List<User> users, @NonNull List<User> selectedUsers, boolean noAssignedUser, @Nullable SelectionListener<User> selectionListener) {
super();
this.avatarSize = avatarSize;
this.account = account;
+ this.users.add(NOT_ASSIGNED);
this.users.addAll(users);
+ if (noAssignedUser) {
+ this.selectedUsers.add(NOT_ASSIGNED);
+ }
this.selectedUsers.addAll(selectedUsers);
this.selectionListener = selectionListener;
setHasStableIds(true);
@@ -43,7 +51,8 @@ public class FilterUserAdapter extends RecyclerView.Adapter<FilterUserAdapter.Us
@Override
public long getItemId(int position) {
- return users.get(position).getLocalId();
+ @Nullable final User user = users.get(position);
+ return user == null ? -1L : user.getLocalId();
}
@NonNull
@@ -54,7 +63,11 @@ public class FilterUserAdapter extends RecyclerView.Adapter<FilterUserAdapter.Us
@Override
public void onBindViewHolder(@NonNull UserViewHolder viewHolder, int position) {
- viewHolder.bind(users.get(position));
+ if (position == 0) {
+ viewHolder.bindNotAssigned();
+ } else {
+ viewHolder.bind(users.get(position));
+ }
}
@Override
@@ -62,10 +75,6 @@ public class FilterUserAdapter extends RecyclerView.Adapter<FilterUserAdapter.Us
return users.size();
}
- public List<User> getSelected() {
- return selectedUsers;
- }
-
class UserViewHolder extends RecyclerView.ViewHolder {
private ItemFilterUserBinding binding;
@@ -74,22 +83,34 @@ public class FilterUserAdapter extends RecyclerView.Adapter<FilterUserAdapter.Us
this.binding = binding;
}
- void bind(final User user) {
- binding.displayName.setText(user.getDisplayname());
+ void bind(@NonNull final User user) {
+ 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.title.setText(itemView.getContext().getString(R.string.simple_unassigned));
+ Glide.with(itemView.getContext())
+ .load(R.drawable.ic_baseline_block_24)
+ .into(binding.avatar);
+ itemView.setSelected(selectedUsers.contains(NOT_ASSIGNED));
+ bindClickListener(NOT_ASSIGNED);
+ }
+ private void bindClickListener(@Nullable User user) {
itemView.setOnClickListener(view -> {
if (selectedUsers.contains(user)) {
selectedUsers.remove(user);
itemView.setSelected(false);
- if(selectionListener != null) {
+ if (selectionListener != null) {
selectionListener.onItemDeselected(user);
}
} else {
selectedUsers.add(user);
itemView.setSelected(true);
- if(selectionListener != null) {
+ if (selectionListener != null) {
selectionListener.onItemSelected(user);
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserFragment.java
index 64bc1db1f..6ffaec6a6 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserFragment.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserFragment.java
@@ -10,14 +10,13 @@ import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
+import it.niedermann.android.util.DimensionUtil;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.DialogFilterAssigneesBinding;
import it.niedermann.nextcloud.deck.model.User;
-import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
import it.niedermann.nextcloud.deck.ui.MainViewModel;
import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce;
-import static it.niedermann.nextcloud.deck.util.DimensionUtil.dpToPx;
import static java.util.Objects.requireNonNull;
public class FilterUserFragment extends Fragment implements SelectionListener<User> {
@@ -33,21 +32,35 @@ public class FilterUserFragment extends Fragment implements SelectionListener<Us
filterViewModel = new ViewModelProvider(requireActivity()).get(FilterViewModel.class);
- observeOnce(new SyncManager(requireContext()).findProposalsForUsersToAssign(mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId()), requireActivity(), (users) -> {
+ observeOnce(filterViewModel.findProposalsForUsersToAssign(mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId()), requireActivity(), (users) -> {
binding.users.setNestedScrollingEnabled(false);
- binding.users.setAdapter(new FilterUserAdapter(dpToPx(requireContext(), R.dimen.avatar_size), mainViewModel.getCurrentAccount(), users, requireNonNull(filterViewModel.getFilterInformationDraft().getValue()).getUsers(), this));
+ binding.users.setAdapter(new FilterUserAdapter(
+ DimensionUtil.INSTANCE.dpToPx(requireContext(), R.dimen.avatar_size),
+ mainViewModel.getCurrentAccount(),
+ users,
+ requireNonNull(filterViewModel.getFilterInformationDraft().getValue()).getUsers(),
+ requireNonNull(filterViewModel.getFilterInformationDraft().getValue()).isNoAssignedUser(),
+ this));
});
return binding.getRoot();
}
@Override
- public void onItemSelected(User item) {
- filterViewModel.addFilterInformationUser(item);
+ public void onItemSelected(@Nullable User item) {
+ if (item == null) {
+ filterViewModel.setNotAssignedUser(true);
+ } else {
+ filterViewModel.addFilterInformationUser(item);
+ }
}
@Override
- public void onItemDeselected(User item) {
- filterViewModel.removeFilterInformationUser(item);
+ public void onItemDeselected(@Nullable User item) {
+ if (item == null) {
+ filterViewModel.setNotAssignedUser(false);
+ } else {
+ filterViewModel.removeFilterInformationUser(item);
+ }
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterViewModel.java
index cf8dc1754..c42a61ebd 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterViewModel.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterViewModel.java
@@ -1,28 +1,40 @@
package it.niedermann.nextcloud.deck.ui.filter;
+import android.app.Application;
+
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
+import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
-import androidx.lifecycle.ViewModel;
+
+import java.util.List;
import it.niedermann.nextcloud.deck.model.Label;
import it.niedermann.nextcloud.deck.model.User;
import it.niedermann.nextcloud.deck.model.enums.EDueType;
import it.niedermann.nextcloud.deck.model.internal.FilterInformation;
+import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
import static it.niedermann.nextcloud.deck.model.internal.FilterInformation.hasActiveFilter;
@SuppressWarnings("WeakerAccess")
-public class FilterViewModel extends ViewModel {
+public class FilterViewModel extends AndroidViewModel {
+
+ private final SyncManager syncManager;
@IntRange(from = 0, to = 2)
private int currentFilterTab = 0;
@NonNull
- private MutableLiveData<FilterInformation> filterInformationDraft = new MutableLiveData<>(new FilterInformation());
+ private final MutableLiveData<FilterInformation> filterInformationDraft = new MutableLiveData<>(new FilterInformation());
@NonNull
- private MutableLiveData<FilterInformation> filterInformation = new MutableLiveData<>();
+ private final MutableLiveData<FilterInformation> filterInformation = new MutableLiveData<>();
+
+ public FilterViewModel(@NonNull Application application) {
+ super(application);
+ this.syncManager = new SyncManager(application);
+ }
public void publishFilterInformationDraft() {
this.filterInformation.postValue(hasActiveFilter(filterInformationDraft.getValue()) ? filterInformationDraft.getValue() : null);
@@ -66,6 +78,18 @@ public class FilterViewModel extends ViewModel {
this.filterInformationDraft.postValue(newDraft);
}
+ public void setNotAssignedUser(boolean notAssignedUser) {
+ FilterInformation newDraft = new FilterInformation(filterInformationDraft.getValue());
+ newDraft.setNoAssignedUser(notAssignedUser);
+ this.filterInformationDraft.postValue(newDraft);
+ }
+
+ public void setNotAssignedLabel(boolean notAssignedLabel) {
+ FilterInformation newDraft = new FilterInformation(filterInformationDraft.getValue());
+ newDraft.setNoAssignedLabel(notAssignedLabel);
+ this.filterInformationDraft.postValue(newDraft);
+ }
+
public void removeFilterInformationLabel(@NonNull Label label) {
FilterInformation newDraft = new FilterInformation(filterInformationDraft.getValue());
newDraft.removeLabel(label);
@@ -86,4 +110,12 @@ public class FilterViewModel extends ViewModel {
public int getCurrentFilterTab() {
return this.currentFilterTab;
}
+
+ public LiveData<List<User>> findProposalsForUsersToAssign(final long accountId, long boardId) {
+ return syncManager.findProposalsForUsersToAssign(accountId, boardId, -1L, -1);
+ }
+
+ public LiveData<List<Label>> findProposalsForLabelsToAssign(final long accountId, final long boardId) {
+ return syncManager.findProposalsForLabelsToAssign(accountId, boardId, -1L);
+ }
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/SelectionListener.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/SelectionListener.java
index d2635a860..3fad71773 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/SelectionListener.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/SelectionListener.java
@@ -1,9 +1,11 @@
package it.niedermann.nextcloud.deck.ui.filter;
+import androidx.annotation.Nullable;
+
public interface SelectionListener<T> {
- void onItemSelected(T item);
+ void onItemSelected(@Nullable T item);
- default void onItemDeselected(T item) {
+ default void onItemDeselected(@Nullable T item) {
// Deselecting is optional
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountViewHolder.java
index 4b43cbed6..0892eb437 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountViewHolder.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountViewHolder.java
@@ -11,14 +11,14 @@ import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.RequestOptions;
-import it.niedermann.android.glidesso.SingleSignOnUrl;
+import it.niedermann.android.util.DimensionUtil;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.ItemAccountChooseBinding;
import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.sso.glide.SingleSignOnUrl;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
-import static it.niedermann.nextcloud.deck.util.DimensionUtil.dpToPx;
public class ManageAccountViewHolder extends RecyclerView.ViewHolder {
@@ -33,7 +33,7 @@ public class ManageAccountViewHolder extends RecyclerView.ViewHolder {
binding.accountName.setText(account.getUserName());
binding.accountHost.setText(Uri.parse(account.getUrl()).getHost());
Glide.with(itemView.getContext())
- .load(new SingleSignOnUrl(account.getName(), account.getAvatarUrl(dpToPx(binding.accountItemAvatar.getContext(), R.dimen.avatar_size))))
+ .load(new SingleSignOnUrl(account.getName(), account.getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(binding.accountItemAvatar.getContext(), R.dimen.avatar_size))))
.placeholder(R.drawable.ic_baseline_account_circle_24)
.error(R.drawable.ic_baseline_account_circle_24)
.apply(RequestOptions.circleCropTransform())
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountsActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountsActivity.java
index 9d273cdcb..8aa45e39a 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountsActivity.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountsActivity.java
@@ -5,15 +5,12 @@ import android.util.Log;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
-
-import com.nextcloud.android.sso.helper.SingleAccountHelper;
+import androidx.lifecycle.ViewModelProvider;
import it.niedermann.nextcloud.deck.databinding.ActivityManageAccountsBinding;
import it.niedermann.nextcloud.deck.model.Account;
-import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
import static it.niedermann.nextcloud.deck.DeckApplication.readCurrentAccountId;
-import static it.niedermann.nextcloud.deck.DeckApplication.saveCurrentAccountId;
import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce;
public class ManageAccountsActivity extends AppCompatActivity {
@@ -21,44 +18,37 @@ public class ManageAccountsActivity extends AppCompatActivity {
private static final String TAG = ManageAccountsActivity.class.getSimpleName();
private ActivityManageAccountsBinding binding;
+ private ManageAccountsViewModel viewModel;
private ManageAccountAdapter adapter;
- private SyncManager syncManager = null;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityManageAccountsBinding.inflate(getLayoutInflater());
- setContentView(binding.getRoot());
+ viewModel = new ViewModelProvider(this).get(ManageAccountsViewModel.class);
+ setContentView(binding.getRoot());
setSupportActionBar(binding.toolbar);
- syncManager = new SyncManager(this);
-
- adapter = new ManageAccountAdapter((account) -> {
- SingleAccountHelper.setCurrentAccount(getApplicationContext(), account.getName());
- syncManager = new SyncManager(this);
- saveCurrentAccountId(this, account.getId());
- }, (accountPair) -> {
+ adapter = new ManageAccountAdapter((account) -> viewModel.setNewAccount(account), (accountPair) -> {
if (accountPair.first != null) {
- syncManager.deleteAccount(accountPair.first.getId());
+ viewModel.deleteAccount(accountPair.first.getId());
} else {
throw new IllegalArgumentException("Could not delete account because given account was null.");
}
Account newAccount = accountPair.second;
if (newAccount != null) {
- SingleAccountHelper.setCurrentAccount(getApplicationContext(), newAccount.getName());
- saveCurrentAccountId(this, newAccount.getId());
- syncManager = new SyncManager(this);
+ viewModel.setNewAccount(newAccount);
} else {
Log.i(TAG, "Got delete account request, but new account is null. Maybe last account has been deleted?");
}
});
binding.accounts.setAdapter(adapter);
- observeOnce(syncManager.readAccount(readCurrentAccountId(this)), this, (account -> {
+ observeOnce(viewModel.readAccount(readCurrentAccountId(this)), this, (account -> {
adapter.setCurrentAccount(account);
- syncManager.readAccounts().observe(this, (localAccounts -> {
+ viewModel.readAccounts().observe(this, (localAccounts -> {
if (localAccounts.size() == 0) {
Log.i(TAG, "No accounts, finishing " + ManageAccountsActivity.class.getSimpleName());
setResult(AppCompatActivity.RESULT_FIRST_USER);
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountsViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountsViewModel.java
new file mode 100644
index 000000000..66e9d3850
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountsViewModel.java
@@ -0,0 +1,45 @@
+package it.niedermann.nextcloud.deck.ui.manageaccounts;
+
+import android.app.Application;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.LiveData;
+
+import com.nextcloud.android.sso.helper.SingleAccountHelper;
+
+import java.util.List;
+
+import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
+
+import static it.niedermann.nextcloud.deck.DeckApplication.saveCurrentAccountId;
+
+@SuppressWarnings("WeakerAccess")
+public class ManageAccountsViewModel extends AndroidViewModel {
+
+ private SyncManager syncManager;
+
+ public ManageAccountsViewModel(@NonNull Application application) {
+ super(application);
+ this.syncManager = new SyncManager(application);
+ }
+
+ public LiveData<Account> readAccount(long id) {
+ return syncManager.readAccount(id);
+ }
+
+ public LiveData<List<Account>> readAccounts() {
+ return syncManager.readAccounts();
+ }
+
+ public void setNewAccount(@NonNull Account account) {
+ SingleAccountHelper.setCurrentAccount(getApplication(), account.getName());
+ syncManager = new SyncManager(getApplication());
+ saveCurrentAccountId(getApplication(), account.getId());
+ }
+
+ public void deleteAccount(long id) {
+ syncManager.deleteAccount(id);
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/movecard/MoveCardDialogFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/movecard/MoveCardDialogFragment.java
new file mode 100644
index 000000000..2b7eb52fe
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/movecard/MoveCardDialogFragment.java
@@ -0,0 +1,128 @@
+package it.niedermann.nextcloud.deck.ui.movecard;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.DialogFragment;
+
+import it.niedermann.nextcloud.deck.DeckLog;
+import it.niedermann.nextcloud.deck.R;
+import it.niedermann.nextcloud.deck.databinding.DialogMoveCardBinding;
+import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.deck.model.Board;
+import it.niedermann.nextcloud.deck.model.Stack;
+import it.niedermann.nextcloud.deck.ui.branding.BrandedDialogFragment;
+import it.niedermann.nextcloud.deck.ui.branding.BrandingUtil;
+import it.niedermann.nextcloud.deck.ui.pickstack.PickStackFragment;
+import it.niedermann.nextcloud.deck.ui.pickstack.PickStackListener;
+import it.niedermann.nextcloud.deck.ui.pickstack.PickStackViewModel;
+
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+
+public class MoveCardDialogFragment extends BrandedDialogFragment implements PickStackListener {
+
+ private static final String KEY_ORIGIN_ACCOUNT_ID = "account_id";
+ private static final String KEY_ORIGIN_BOARD_LOCAL_ID = "board_local_id";
+ private static final String KEY_ORIGIN_CARD_TITLE = "card_title";
+ private static final String KEY_ORIGIN_CARD_LOCAL_ID = "card_local_id";
+ private Long originAccountId;
+ private Long originBoardLocalId;
+ private String originCardTitle;
+ private Long originCardLocalId;
+
+ private DialogMoveCardBinding binding;
+ private PickStackViewModel viewModel;
+ private MoveCardListener moveCardListener;
+
+ private Account selectedAccount;
+ private Board selectedBoard;
+ private Stack selectedStack;
+
+ @Override
+ public void onAttach(@NonNull Context context) {
+ super.onAttach(context);
+ if (getParentFragment() instanceof MoveCardListener) {
+ this.moveCardListener = (MoveCardListener) getParentFragment();
+ } else if (context instanceof MoveCardListener) {
+ this.moveCardListener = (MoveCardListener) context;
+ } else {
+ throw new IllegalArgumentException("Caller must implement " + MoveCardListener.class.getSimpleName());
+ }
+
+ final Bundle args = requireArguments();
+ originAccountId = args.getLong(KEY_ORIGIN_ACCOUNT_ID, -1L);
+ if (originAccountId < 0) {
+ throw new IllegalArgumentException("Missing " + KEY_ORIGIN_ACCOUNT_ID);
+ }
+ originCardLocalId = args.getLong(KEY_ORIGIN_CARD_LOCAL_ID, -1L);
+ if (originCardLocalId < 0) {
+ throw new IllegalArgumentException("Missing " + KEY_ORIGIN_CARD_LOCAL_ID);
+ }
+ originBoardLocalId = args.getLong(KEY_ORIGIN_BOARD_LOCAL_ID, -1L);
+ if (originBoardLocalId < 0) {
+ throw new IllegalArgumentException("Missing " + KEY_ORIGIN_BOARD_LOCAL_ID);
+ }
+ originCardTitle = args.getString(KEY_ORIGIN_CARD_TITLE);
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ binding = DialogMoveCardBinding.inflate(inflater);
+ binding.title.setText(getString(R.string.action_card_move_title, originCardTitle));
+ binding.submit.setOnClickListener((v) -> {
+ DeckLog.verbose("[Move card] Attempt to move to " + Stack.class.getSimpleName() + " #" + selectedStack.getLocalId());
+ this.moveCardListener.move(originAccountId, originCardLocalId, selectedAccount.getId(), selectedBoard.getLocalId(), selectedStack.getLocalId());
+ dismiss();
+ });
+ binding.cancel.setOnClickListener((v) -> dismiss());
+ return binding.getRoot();
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ getChildFragmentManager()
+ .beginTransaction()
+ .add(R.id.fragment_container, PickStackFragment.newInstance(false))
+ .commit();
+ }
+
+ @Override
+ public void onStackPicked(@NonNull Account account, @Nullable Board board, @Nullable Stack stack) {
+ this.selectedAccount = account;
+ this.selectedBoard = board;
+ this.selectedStack = stack;
+ if (board == null || stack == null) {
+ binding.submit.setEnabled(false);
+ binding.moveWarning.setVisibility(GONE);
+ } else {
+ binding.submit.setEnabled(true);
+ binding.moveWarning.setVisibility(board.getLocalId().equals(originBoardLocalId) ? GONE : VISIBLE);
+ }
+ }
+
+ @Override
+ public void applyBrand(int mainColor) {
+ final ColorStateList mainColorStateList = ColorStateList.valueOf(BrandingUtil.getSecondaryForegroundColorDependingOnTheme(requireContext(), mainColor));
+ binding.cancel.setTextColor(mainColorStateList);
+ binding.submit.setTextColor(mainColorStateList);
+ }
+
+ public static DialogFragment newInstance(long originAccountId, long originBoardLocalId, String originCardTitle, Long originCardLocalId) {
+ final DialogFragment dialogFragment = new MoveCardDialogFragment();
+ final Bundle args = new Bundle();
+ args.putLong(KEY_ORIGIN_ACCOUNT_ID, originAccountId);
+ args.putLong(KEY_ORIGIN_BOARD_LOCAL_ID, originBoardLocalId);
+ args.putString(KEY_ORIGIN_CARD_TITLE, originCardTitle);
+ args.putLong(KEY_ORIGIN_CARD_LOCAL_ID, originCardLocalId);
+ dialogFragment.setArguments(args);
+ return dialogFragment;
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/movecard/MoveCardListener.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/movecard/MoveCardListener.java
new file mode 100644
index 000000000..f6f7a7a1f
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/movecard/MoveCardListener.java
@@ -0,0 +1,5 @@
+package it.niedermann.nextcloud.deck.ui.movecard;
+
+public interface MoveCardListener {
+ void move(long originAccountId, long originCardLocalId, long targetAccountId, long targetBoardLocalId, long targetStackLocalId);
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackFragment.java
new file mode 100644
index 000000000..d65971cf4
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackFragment.java
@@ -0,0 +1,204 @@
+package it.niedermann.nextcloud.deck.ui.pickstack;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.Observer;
+import androidx.lifecycle.ViewModelProvider;
+
+import java.util.List;
+
+import it.niedermann.nextcloud.deck.databinding.FragmentPickStackBinding;
+import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.deck.model.Board;
+import it.niedermann.nextcloud.deck.model.Stack;
+import it.niedermann.nextcloud.deck.ui.ImportAccountActivity;
+import it.niedermann.nextcloud.deck.ui.preparecreate.AccountAdapter;
+import it.niedermann.nextcloud.deck.ui.preparecreate.BoardAdapter;
+import it.niedermann.nextcloud.deck.ui.preparecreate.SelectedListener;
+import it.niedermann.nextcloud.deck.ui.preparecreate.StackAdapter;
+
+import static androidx.lifecycle.Transformations.switchMap;
+import static it.niedermann.nextcloud.deck.DeckApplication.readCurrentAccountId;
+import static it.niedermann.nextcloud.deck.DeckApplication.readCurrentBoardId;
+import static it.niedermann.nextcloud.deck.DeckApplication.readCurrentStackId;
+
+public class PickStackFragment extends Fragment {
+
+ private FragmentPickStackBinding binding;
+ private PickStackViewModel viewModel;
+
+ private static final String KEY_SHOW_BOARDS_WITHOUT_EDIT_PERMISSION = "show_boards_without_edit_permission";
+
+ private PickStackListener pickStackListener;
+
+ private boolean showBoardsWithoutEditPermission = false;
+ private long lastAccountId;
+ private long lastBoardId;
+ private long lastStackId;
+
+ private ArrayAdapter<Account> accountAdapter;
+ private ArrayAdapter<Board> boardAdapter;
+ private ArrayAdapter<Stack> stackAdapter;
+
+ @Nullable
+ private LiveData<List<Board>> boardsLiveData;
+ @NonNull
+ private Observer<List<Board>> boardsObserver = (boards) -> {
+ boardAdapter.clear();
+ boardAdapter.addAll(boards);
+ binding.boardSelect.setEnabled(true);
+
+ if (boards.size() > 0) {
+ binding.boardSelect.setEnabled(true);
+
+ Board boardToSelect = null;
+ for (Board board : boards) {
+ if (board.getLocalId() == lastBoardId) {
+ boardToSelect = board;
+ break;
+ }
+ }
+ if (boardToSelect == null) {
+ boardToSelect = boards.get(0);
+ }
+ binding.boardSelect.setSelection(boardAdapter.getPosition(boardToSelect));
+ } else {
+ binding.boardSelect.setEnabled(false);
+ pickStackListener.onStackPicked((Account) binding.accountSelect.getSelectedItem(), null, null);
+ }
+ };
+
+ @Nullable
+ private LiveData<List<Stack>> stacksLiveData;
+ @NonNull
+ private Observer<List<Stack>> stacksObserver = (stacks) -> {
+ stackAdapter.clear();
+ stackAdapter.addAll(stacks);
+
+ if (stacks.size() > 0) {
+ binding.stackSelect.setEnabled(true);
+
+ Stack stackToSelect = null;
+ for (Stack stack : stacks) {
+ if (stack.getLocalId() == lastStackId) {
+ stackToSelect = stack;
+ break;
+ }
+ }
+ if (stackToSelect == null) {
+ stackToSelect = stacks.get(0);
+ }
+ binding.stackSelect.setSelection(stackAdapter.getPosition(stackToSelect));
+ } else {
+ binding.stackSelect.setEnabled(false);
+ pickStackListener.onStackPicked((Account) binding.accountSelect.getSelectedItem(), (Board) binding.boardSelect.getSelectedItem(), null);
+ }
+ };
+
+ @Override
+ public void onAttach(@NonNull Context context) {
+ super.onAttach(context);
+ if (getParentFragment() instanceof PickStackListener) {
+ this.pickStackListener = (PickStackListener) getParentFragment();
+ } else if (context instanceof PickStackListener) {
+ this.pickStackListener = (PickStackListener) context;
+ } else {
+ throw new IllegalArgumentException("Caller must implement " + PickStackListener.class.getSimpleName());
+ }
+ final Bundle args = getArguments();
+ if (args != null) {
+ this.showBoardsWithoutEditPermission = args.getBoolean(KEY_SHOW_BOARDS_WITHOUT_EDIT_PERMISSION, false);
+ }
+ }
+
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ binding = FragmentPickStackBinding.inflate(getLayoutInflater());
+ viewModel = new ViewModelProvider(requireActivity()).get(PickStackViewModel.class);
+
+ accountAdapter = new AccountAdapter(requireContext());
+ binding.accountSelect.setAdapter(accountAdapter);
+ binding.accountSelect.setEnabled(false);
+ boardAdapter = new BoardAdapter(requireContext());
+ binding.boardSelect.setAdapter(boardAdapter);
+ binding.stackSelect.setEnabled(false);
+ stackAdapter = new StackAdapter(requireContext());
+ binding.stackSelect.setAdapter(stackAdapter);
+ binding.stackSelect.setEnabled(false);
+
+ switchMap(viewModel.hasAccounts(), hasAccounts -> {
+ if (hasAccounts) {
+ return viewModel.readAccounts();
+ } else {
+ startActivityForResult(new Intent(requireActivity(), ImportAccountActivity.class), ImportAccountActivity.REQUEST_CODE_IMPORT_ACCOUNT);
+ return null;
+ }
+ }).observe(getViewLifecycleOwner(), (List<Account> accounts) -> {
+ if (accounts == null || accounts.size() == 0) {
+ throw new IllegalStateException("hasAccounts() returns true, but readAccounts() returns null or has no entry");
+ }
+
+ lastAccountId = readCurrentAccountId(requireContext());
+ lastBoardId = readCurrentBoardId(requireContext(), lastAccountId);
+ lastStackId = readCurrentStackId(requireContext(), lastAccountId, lastBoardId);
+
+ accountAdapter.clear();
+ accountAdapter.addAll(accounts);
+ binding.accountSelect.setEnabled(true);
+
+ for (Account account : accounts) {
+ if (account.getId() == lastAccountId) {
+ binding.accountSelect.setSelection(accountAdapter.getPosition(account));
+ break;
+ }
+ }
+ });
+
+ binding.accountSelect.setOnItemSelectedListener((SelectedListener) (parent, view, position, id) -> {
+ updateLiveDataSource(boardsLiveData, boardsObserver, showBoardsWithoutEditPermission
+ ? viewModel.getBoards(parent.getSelectedItemId())
+ : viewModel.getBoardsWithEditPermission(parent.getSelectedItemId()));
+ });
+
+ binding.boardSelect.setOnItemSelectedListener((SelectedListener) (parent, view, position, id) -> {
+ updateLiveDataSource(stacksLiveData, stacksObserver, viewModel.getStacksForBoard(binding.accountSelect.getSelectedItemId(), parent.getSelectedItemId()));
+ });
+
+ binding.stackSelect.setOnItemSelectedListener((SelectedListener) (parent, view, position, id) -> {
+ pickStackListener.onStackPicked((Account) binding.accountSelect.getSelectedItem(), (Board) binding.boardSelect.getSelectedItem(), (Stack) parent.getSelectedItem());
+ });
+
+ return binding.getRoot();
+ }
+
+ /**
+ * Updates the source of the given liveData and de- and reregisters the given observer.
+ */
+ private <T> void updateLiveDataSource(@Nullable LiveData<T> liveData, Observer<T> observer, LiveData<T> newSource) {
+ if (liveData != null) {
+ liveData.removeObserver(observer);
+ }
+ liveData = newSource;
+ liveData.observe(getViewLifecycleOwner(), observer);
+ }
+
+ public static PickStackFragment newInstance(boolean showBoardsWithoutEditPermission) {
+ final PickStackFragment fragment = new PickStackFragment();
+ final Bundle args = new Bundle();
+ args.putBoolean(KEY_SHOW_BOARDS_WITHOUT_EDIT_PERMISSION, showBoardsWithoutEditPermission);
+ fragment.setArguments(args);
+ return fragment;
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackListener.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackListener.java
new file mode 100644
index 000000000..ce227746a
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackListener.java
@@ -0,0 +1,12 @@
+package it.niedermann.nextcloud.deck.ui.pickstack;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.deck.model.Board;
+import it.niedermann.nextcloud.deck.model.Stack;
+
+public interface PickStackListener {
+ void onStackPicked(@NonNull Account account, @Nullable Board board, @Nullable Stack stack);
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackViewModel.java
new file mode 100644
index 000000000..cc9fc2259
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackViewModel.java
@@ -0,0 +1,45 @@
+package it.niedermann.nextcloud.deck.ui.pickstack;
+
+import android.app.Application;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.LiveData;
+
+import java.util.List;
+
+import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.deck.model.Board;
+import it.niedermann.nextcloud.deck.model.Stack;
+import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
+
+@SuppressWarnings("WeakerAccess")
+public class PickStackViewModel extends AndroidViewModel {
+
+ private final SyncManager syncManager;
+
+ public PickStackViewModel(@NonNull Application application) {
+ super(application);
+ this.syncManager = new SyncManager(application);
+ }
+
+ public LiveData<Boolean> hasAccounts() {
+ return syncManager.hasAccounts();
+ }
+
+ public LiveData<List<Account>> readAccounts() {
+ return syncManager.readAccounts();
+ }
+
+ public LiveData<List<Board>> getBoards(long accountId) {
+ return syncManager.getBoards(accountId);
+ }
+
+ public LiveData<List<Board>> getBoardsWithEditPermission(long accountId) {
+ return syncManager.getBoardsWithEditPermission(accountId);
+ }
+
+ public LiveData<List<Stack>> getStacksForBoard(long accountId, long localBoardId) {
+ return syncManager.getStacksForBoard(accountId, localBoardId);
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/AccountAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/AccountAdapter.java
index 35a98efd7..f537c9fb4 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/AccountAdapter.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/AccountAdapter.java
@@ -6,14 +6,17 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
-import org.jetbrains.annotations.NotNull;
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.request.RequestOptions;
+import java.net.URL;
+
+import it.niedermann.android.util.DimensionUtil;
import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.ItemPrepareCreateAccountBinding;
import it.niedermann.nextcloud.deck.model.Account;
-import it.niedermann.nextcloud.deck.util.DimensionUtil;
-import it.niedermann.nextcloud.deck.util.ViewUtil;
+import it.niedermann.nextcloud.sso.glide.SingleSignOnUrl;
public class AccountAdapter extends AbstractAdapter<Account> {
@@ -27,9 +30,9 @@ public class AccountAdapter extends AbstractAdapter<Account> {
return item.getId();
}
- @NotNull
+ @NonNull
@Override
- public View getView(int position, View convertView, @NotNull ViewGroup parent) {
+ public View getView(int position, View convertView, @NonNull ViewGroup parent) {
final ItemPrepareCreateAccountBinding binding;
if (convertView == null) {
binding = ItemPrepareCreateAccountBinding.inflate(inflater, parent, false);
@@ -40,8 +43,18 @@ public class AccountAdapter extends AbstractAdapter<Account> {
final Account item = getItem(position);
if (item != null) {
binding.username.setText(item.getUserName());
- binding.instance.setText(item.getUrl());
- ViewUtil.addAvatar(binding.avatar, item.getUrl(), item.getUserName(), DimensionUtil.dpToPx(binding.avatar.getContext(), R.dimen.icon_size_details), R.drawable.ic_person_grey600_24dp);
+ try {
+ binding.instance.setText(new URL(item.getUrl()).getHost());
+ } catch (Throwable t) {
+ binding.instance.setText(item.getUrl());
+ }
+
+ Glide.with(getContext())
+ .load(new SingleSignOnUrl(item.getName(), item.getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(binding.avatar.getContext(), R.dimen.icon_size_details))))
+ .placeholder(R.drawable.ic_baseline_account_circle_24)
+ .error(R.drawable.ic_baseline_account_circle_24)
+ .apply(RequestOptions.circleCropTransform())
+ .into(binding.avatar);
} else {
DeckLog.logError(new IllegalArgumentException("No item for position " + position));
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/BoardAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/BoardAdapter.java
index 190c55e4b..c27fa04ee 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/BoardAdapter.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/BoardAdapter.java
@@ -6,8 +6,6 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
-import org.jetbrains.annotations.NotNull;
-
import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.ItemPrepareCreateBoardBinding;
@@ -26,9 +24,9 @@ public class BoardAdapter extends AbstractAdapter<Board> {
return item.getLocalId();
}
- @NotNull
+ @NonNull
@Override
- public View getView(int position, View convertView, @NotNull ViewGroup parent) {
+ public View getView(int position, View convertView, @NonNull ViewGroup parent) {
final ItemPrepareCreateBoardBinding binding;
if (convertView == null) {
binding = ItemPrepareCreateBoardBinding.inflate(inflater, parent, false);
@@ -39,7 +37,7 @@ public class BoardAdapter extends AbstractAdapter<Board> {
final Board item = getItem(position);
if (item != null) {
binding.boardTitle.setText(item.getTitle());
- binding.avatar.setImageDrawable(ViewUtil.getTintedImageView(binding.avatar.getContext(), R.drawable.circle_grey600_36dp, "#" + item.getColor()));
+ binding.avatar.setImageDrawable(ViewUtil.getTintedImageView(binding.avatar.getContext(), R.drawable.circle_grey600_36dp, item.getColor()));
} else {
DeckLog.logError(new IllegalArgumentException("No item for position " + position));
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/PrepareCreateActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/PrepareCreateActivity.java
index ab0cc4816..18317e078 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/PrepareCreateActivity.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/PrepareCreateActivity.java
@@ -2,214 +2,52 @@ package it.niedermann.nextcloud.deck.ui.preparecreate;
import android.content.ClipData;
import android.content.Intent;
-import android.content.res.ColorStateList;
-import android.graphics.Color;
import android.os.Bundle;
import android.text.TextUtils;
-import android.widget.ArrayAdapter;
-import androidx.annotation.ColorInt;
-import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.core.content.ContextCompat;
-import androidx.core.graphics.drawable.DrawableCompat;
-import androidx.lifecycle.LiveData;
-import androidx.lifecycle.Observer;
+import androidx.appcompat.app.ActionBar;
-import java.util.List;
-
-import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.R;
-import it.niedermann.nextcloud.deck.databinding.ActivityPrepareCreateBinding;
import it.niedermann.nextcloud.deck.model.Account;
-import it.niedermann.nextcloud.deck.model.Board;
-import it.niedermann.nextcloud.deck.model.full.FullStack;
-import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
-import it.niedermann.nextcloud.deck.ui.ImportAccountActivity;
-import it.niedermann.nextcloud.deck.ui.branding.Branded;
+import it.niedermann.nextcloud.deck.ui.PickStackActivity;
import it.niedermann.nextcloud.deck.ui.card.EditActivity;
-import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment;
-import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler;
-import it.niedermann.nextcloud.deck.util.ColorUtil;
-import static android.graphics.Color.parseColor;
-import static androidx.lifecycle.Transformations.switchMap;
-import static it.niedermann.nextcloud.deck.DeckApplication.isDarkTheme;
-import static it.niedermann.nextcloud.deck.DeckApplication.readCurrentAccountId;
-import static it.niedermann.nextcloud.deck.DeckApplication.readCurrentBoardId;
-import static it.niedermann.nextcloud.deck.DeckApplication.readCurrentStackId;
import static it.niedermann.nextcloud.deck.DeckApplication.saveCurrentAccountId;
import static it.niedermann.nextcloud.deck.DeckApplication.saveCurrentBoardId;
import static it.niedermann.nextcloud.deck.DeckApplication.saveCurrentStackId;
-import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.getSecondaryForegroundColorDependingOnTheme;
-import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.isBrandingEnabled;
-import static it.niedermann.nextcloud.deck.util.ColorUtil.contrastRatioIsSufficientBigAreas;
-
-public class PrepareCreateActivity extends AppCompatActivity implements Branded {
-
- private ActivityPrepareCreateBinding binding;
-
- private SyncManager syncManager;
-
- private boolean brandingEnabled;
-
- private long lastAccountId;
- private long lastBoardId;
- private long lastStackId;
- private ArrayAdapter<Account> accountAdapter;
- private ArrayAdapter<Board> boardAdapter;
- private ArrayAdapter<FullStack> stackAdapter;
-
- @Nullable
- private LiveData<List<Board>> boardsLiveData;
- @NonNull
- private Observer<List<Board>> boardsObserver = (boards) -> {
- boardAdapter.clear();
- boardAdapter.addAll(boards);
- binding.boardSelect.setEnabled(true);
-
- if (boards.size() > 0) {
- binding.boardSelect.setEnabled(true);
-
- for (Board board : boards) {
- if (board.getLocalId() == lastBoardId) {
- binding.boardSelect.setSelection(boardAdapter.getPosition(board));
- applyBrand(Color.parseColor('#' + board.getColor()));
- break;
- }
- }
- } else {
- applyBrand(ContextCompat.getColor(this, R.color.defaultBrand));
- binding.boardSelect.setEnabled(false);
- binding.submit.setEnabled(false);
- }
- };
-
- @Nullable
- private LiveData<List<FullStack>> stacksLiveData;
- @NonNull
- private Observer<List<FullStack>> stacksObserver = (fullStacks) -> {
- stackAdapter.clear();
- stackAdapter.addAll(fullStacks);
-
- if (fullStacks.size() > 0) {
- binding.stackSelect.setEnabled(true);
- binding.submit.setEnabled(true);
-
- for (FullStack fullStack : fullStacks) {
- if (fullStack.getLocalId() == lastStackId) {
- binding.stackSelect.setSelection(stackAdapter.getPosition(fullStack));
- break;
- }
- }
- } else {
- binding.stackSelect.setEnabled(false);
- binding.submit.setEnabled(false);
- }
- };
+public class PrepareCreateActivity extends PickStackActivity {
@Override
- protected void onCreate(Bundle savedInstanceState) {
+ public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
-
- Thread.setDefaultUncaughtExceptionHandler(new ExceptionHandler(this));
-
- brandingEnabled = isBrandingEnabled(this);
-
- binding = ActivityPrepareCreateBinding.inflate(getLayoutInflater());
- setContentView(binding.getRoot());
- setSupportActionBar(binding.toolbar);
-
- accountAdapter = new AccountAdapter(this);
- binding.accountSelect.setAdapter(accountAdapter);
- binding.accountSelect.setEnabled(false);
- boardAdapter = new BoardAdapter(this);
- binding.boardSelect.setAdapter(boardAdapter);
- binding.stackSelect.setEnabled(false);
- stackAdapter = new StackAdapter(this);
- binding.stackSelect.setAdapter(stackAdapter);
- binding.stackSelect.setEnabled(false);
-
- syncManager = new SyncManager(this);
-
- switchMap(syncManager.hasAccounts(), hasAccounts -> {
- if (hasAccounts) {
- return syncManager.readAccounts();
- } else {
- startActivityForResult(new Intent(this, ImportAccountActivity.class), ImportAccountActivity.REQUEST_CODE_IMPORT_ACCOUNT);
- return null;
- }
- }).observe(this, (List<Account> accounts) -> {
- if (accounts == null || accounts.size() == 0) {
- throw new IllegalStateException("hasAccounts() returns true, but readAccounts() returns null or has no entry");
- }
-
- lastAccountId = readCurrentAccountId(this);
- lastBoardId = readCurrentBoardId(this, lastAccountId);
- lastStackId = readCurrentStackId(this, lastAccountId, lastBoardId);
-
- accountAdapter.clear();
- accountAdapter.addAll(accounts);
- binding.accountSelect.setEnabled(true);
-
- for (Account account : accounts) {
- if (account.getId() == lastAccountId) {
- binding.accountSelect.setSelection(accountAdapter.getPosition(account));
- break;
- }
- }
- });
-
- binding.accountSelect.setOnItemSelectedListener((SelectedListener) (parent, view, position, id) -> {
- updateLiveDataSource(boardsLiveData, boardsObserver, syncManager.getBoardsWithEditPermission(parent.getSelectedItemId()));
- });
-
- binding.boardSelect.setOnItemSelectedListener((SelectedListener) (parent, view, position, id) -> {
- applyBrand(Color.parseColor('#' + ((Board) binding.boardSelect.getSelectedItem()).getColor()));
- updateLiveDataSource(stacksLiveData, stacksObserver, syncManager.getStacksForBoard(binding.accountSelect.getSelectedItemId(), parent.getSelectedItemId()));
- });
-
- binding.cancel.setOnClickListener((v) -> finish());
- binding.submit.setOnClickListener((v) -> onSubmit());
+ final ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setTitle(R.string.add_card);
+ }
}
- /**
- * Updates the source of the given liveData and de- and reregisters the given observer.
- */
- private <T> void updateLiveDataSource(@Nullable LiveData<T> liveData, Observer<T> observer, LiveData<T> newSource) {
- if (liveData != null) {
- liveData.removeObserver(observer);
+ @Override
+ protected void onSubmit(Account account, long boardId, long stackId) {
+ final String receivedClipData = getReceivedClipData(getIntent());
+ if (receivedClipData == null) {
+ startActivity(EditActivity.createNewCardIntent(this, account, boardId, stackId));
+ } else {
+ startActivity(EditActivity.createNewCardIntent(this, account, boardId, stackId, receivedClipData));
}
- liveData = newSource;
- liveData.observe(PrepareCreateActivity.this, observer);
- }
- /**
- * Starts EditActivity and passes parameters.
- */
- private void onSubmit() {
- final Account account = accountAdapter.getItem(binding.accountSelect.getSelectedItemPosition());
- if (account != null) {
- final long boardId = binding.boardSelect.getSelectedItemId();
- final long stackId = binding.stackSelect.getSelectedItemId();
- final String receivedClipData = getReceivedClipData(getIntent());
- if (receivedClipData == null) {
- startActivity(EditActivity.createNewCardIntent(this, account, boardId, stackId));
- } else {
- startActivity(EditActivity.createNewCardIntent(this, account, boardId, stackId, receivedClipData));
- }
+ saveCurrentAccountId(this, account.getId());
+ saveCurrentBoardId(this, account.getId(), boardId);
+ saveCurrentStackId(this, account.getId(), boardId, stackId);
+ applyBrand(account.getColor());
- saveCurrentAccountId(this, account.getId());
- saveCurrentBoardId(this, account.getId(), boardId);
- saveCurrentStackId(this, account.getId(), boardId, stackId);
- applyBrand(parseColor(account.getColor()));
+ finish();
+ }
- finish();
- } else {
- ExceptionDialogFragment.newInstance(new IllegalStateException("Selected account at position " + binding.accountSelect.getSelectedItemPosition() + " is null."), null).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
- }
+ @Override
+ protected boolean showBoardsWithoutEditPermission() {
+ return false;
}
@Nullable
@@ -232,20 +70,4 @@ public class PrepareCreateActivity extends AppCompatActivity implements Branded
final CharSequence text = item.getText();
return TextUtils.isEmpty(text) ? null : text.toString();
}
-
- @Override
- public void applyBrand(int mainColor) {
- try {
- if (brandingEnabled) {
- @ColorInt final int finalMainColor = contrastRatioIsSufficientBigAreas(mainColor, ContextCompat.getColor(this, R.color.primary))
- ? mainColor
- : isDarkTheme(this) ? Color.WHITE : Color.BLACK;
- DrawableCompat.setTintList(binding.submit.getBackground(), ColorStateList.valueOf(finalMainColor));
- binding.submit.setTextColor(ColorUtil.getForegroundColorForBackgroundColor(finalMainColor));
- binding.cancel.setTextColor(getSecondaryForegroundColorDependingOnTheme(this, mainColor));
- }
- } catch (Throwable t) {
- DeckLog.logError(t);
- }
- }
} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/StackAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/StackAdapter.java
index 89c702075..e3e0ad5b5 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/StackAdapter.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/StackAdapter.java
@@ -6,14 +6,12 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
-import org.jetbrains.annotations.NotNull;
-
import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.ItemPrepareCreateStackBinding;
-import it.niedermann.nextcloud.deck.model.full.FullStack;
+import it.niedermann.nextcloud.deck.model.Stack;
-public class StackAdapter extends AbstractAdapter<FullStack> {
+public class StackAdapter extends AbstractAdapter<Stack> {
@SuppressWarnings("WeakerAccess")
public StackAdapter(@NonNull Context context) {
@@ -21,13 +19,13 @@ public class StackAdapter extends AbstractAdapter<FullStack> {
}
@Override
- protected long getItemId(@NonNull FullStack item) {
+ protected long getItemId(@NonNull Stack item) {
return item.getLocalId();
}
- @NotNull
+ @NonNull
@Override
- public View getView(int position, View convertView, @NotNull ViewGroup parent) {
+ public View getView(int position, View convertView, @NonNull ViewGroup parent) {
final ItemPrepareCreateStackBinding binding;
if (convertView == null) {
binding = ItemPrepareCreateStackBinding.inflate(inflater, parent, false);
@@ -35,9 +33,9 @@ public class StackAdapter extends AbstractAdapter<FullStack> {
binding = ItemPrepareCreateStackBinding.bind(convertView);
}
- final FullStack item = getItem(position);
+ final Stack item = getItem(position);
if (item != null) {
- binding.stackTitle.setText(item.getStack().getTitle());
+ binding.stackTitle.setText(item.getTitle());
} else {
DeckLog.logError(new IllegalArgumentException("No item for position " + position));
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/settings/SettingsFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/settings/SettingsFragment.java
index ea8d90f22..04429f225 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/settings/SettingsFragment.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/settings/SettingsFragment.java
@@ -23,6 +23,7 @@ public class SettingsFragment extends PreferenceFragmentCompat implements Brande
private BrandedSwitchPreference wifiOnlyPref;
private BrandedSwitchPreference themePref;
private BrandedSwitchPreference brandingPref;
+ private BrandedSwitchPreference compactPref;
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
@@ -67,6 +68,8 @@ public class SettingsFragment extends PreferenceFragmentCompat implements Brande
DeckLog.error("Could not find preference with key: \"" + getString(R.string.pref_key_dark_theme) + "\"");
}
+ compactPref = findPreference(getString(R.string.pref_key_compact));
+
final ListPreference backgroundSyncPref = findPreference(getString(R.string.pref_key_background_sync));
if (backgroundSyncPref != null) {
backgroundSyncPref.setOnPreferenceChangeListener((Preference preference, Object newValue) -> {
@@ -92,5 +95,6 @@ public class SettingsFragment extends PreferenceFragmentCompat implements Brande
wifiOnlyPref.applyBrand(mainColor);
themePref.applyBrand(mainColor);
brandingPref.applyBrand(mainColor);
+ compactPref.applyBrand(mainColor);
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/sharetarget/ShareProgressDialogFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/sharetarget/ShareProgressDialogFragment.java
index 271b60489..3028ea952 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/sharetarget/ShareProgressDialogFragment.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/sharetarget/ShareProgressDialogFragment.java
@@ -14,15 +14,16 @@ import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.lifecycle.ViewModelProvider;
+import it.niedermann.nextcloud.deck.BuildConfig;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.DialogShareProgressBinding;
import it.niedermann.nextcloud.deck.exceptions.UploadAttachmentFailedException;
import it.niedermann.nextcloud.deck.ui.branding.BrandedDialogFragment;
import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment;
+import it.niedermann.nextcloud.exception.ExceptionUtil;
import static android.graphics.PorterDuff.Mode;
import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.getSecondaryForegroundColorDependingOnTheme;
-import static it.niedermann.nextcloud.deck.util.ExceptionUtil.getDebugInfos;
public class ShareProgressDialogFragment extends BrandedDialogFragment {
@@ -70,7 +71,7 @@ public class ShareProgressDialogFragment extends BrandedDialogFragment {
binding.errorReportButton.setOnClickListener((v) -> {
final StringBuilder debugInfos = new StringBuilder(exceptionsCount + " attachments failed to upload:");
for (Throwable t : exceptions) {
- debugInfos.append(getDebugInfos(requireContext(), t, null));
+ debugInfos.append(ExceptionUtil.INSTANCE.getDebugInfos(requireContext(), t, BuildConfig.FLAVOR));
}
ExceptionDialogFragment.newInstance(new UploadAttachmentFailedException(debugInfos.toString()), null)
.show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/sharetarget/ShareTargetActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/sharetarget/ShareTargetActivity.java
index a64629bd7..3a0005fb1 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/sharetarget/ShareTargetActivity.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/sharetarget/ShareTargetActivity.java
@@ -16,9 +16,9 @@ import androidx.lifecycle.ViewModelProvider;
import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException;
import java.io.File;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
-import java.util.Date;
import java.util.List;
import it.niedermann.nextcloud.deck.DeckLog;
@@ -126,7 +126,7 @@ public class ShareTargetActivity extends MainActivity implements SelectCardListe
throw new IllegalArgumentException("MimeType of uri is null. [" + uri + "]");
}
runOnUiThread(() -> {
- final WrappedLiveData<Attachment> liveData = syncManager.addAttachmentToCard(fullCard.getAccountId(), fullCard.getCard().getLocalId(), mimeType, tempFile);
+ final WrappedLiveData<Attachment> liveData = mainViewModel.addAttachmentToCard(fullCard.getAccountId(), fullCard.getCard().getLocalId(), mimeType, tempFile);
liveData.observe(ShareTargetActivity.this, (next) -> {
if (liveData.hasError()) {
if (liveData.getError() instanceof NextcloudHttpRequestFailedException && ((NextcloudHttpRequestFailedException) liveData.getError()).getStatusCode() == HTTP_CONFLICT) {
@@ -160,7 +160,7 @@ public class ShareTargetActivity extends MainActivity implements SelectCardListe
? receivedText
: oldDescription + "\n\n" + receivedText
);
- WrappedLiveData<FullCard> liveData = syncManager.updateCard(fullCard);
+ WrappedLiveData<FullCard> liveData = mainViewModel.updateCard(fullCard);
observeOnce(liveData, this, (next) -> {
if (liveData.hasError()) {
cardSelected = false;
@@ -173,8 +173,8 @@ public class ShareTargetActivity extends MainActivity implements SelectCardListe
break;
case 1:
final Account currentAccount = mainViewModel.getCurrentAccount();
- final DeckComment comment = new DeckComment(receivedText.trim(), currentAccount.getUserName(), new Date());
- syncManager.addCommentToCard(currentAccount.getId(), fullCard.getLocalId(), comment);
+ final DeckComment comment = new DeckComment(receivedText.trim(), currentAccount.getUserName(), Instant.now());
+ mainViewModel.addCommentToCard(currentAccount.getId(), fullCard.getLocalId(), comment);
Toast.makeText(getApplicationContext(), getString(R.string.share_success, "\"" + receivedText + "\"", "\"" + fullCard.getCard().getTitle() + "\""), Toast.LENGTH_LONG).show();
finish();
break;
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/StackAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/StackAdapter.java
index e2cc83372..8fc56759f 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/StackAdapter.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/StackAdapter.java
@@ -8,11 +8,11 @@ import androidx.viewpager2.adapter.FragmentStateAdapter;
import java.util.ArrayList;
import java.util.List;
-import it.niedermann.nextcloud.deck.model.full.FullStack;
+import it.niedermann.nextcloud.deck.model.Stack;
public class StackAdapter extends FragmentStateAdapter {
@NonNull
- private List<FullStack> stackList = new ArrayList<>();
+ private final List<Stack> stackList = new ArrayList<>();
public StackAdapter(@NonNull FragmentActivity fragmentActivity) {
super(fragmentActivity);
@@ -23,7 +23,7 @@ public class StackAdapter extends FragmentStateAdapter {
return stackList.size();
}
- public FullStack getItem(int position) {
+ public Stack getItem(int position) {
return stackList.get(position);
}
@@ -34,7 +34,7 @@ public class StackAdapter extends FragmentStateAdapter {
@Override
public boolean containsItem(long itemId) {
- for (FullStack stack : stackList) {
+ for (Stack stack : stackList) {
if (stack.getLocalId() == itemId) {
return true;
}
@@ -48,9 +48,9 @@ public class StackAdapter extends FragmentStateAdapter {
return StackFragment.newInstance(stackList.get(position).getLocalId());
}
- public void setStacks(@NonNull List<FullStack> fullStacks) {
+ public void setStacks(@NonNull List<Stack> stacks) {
this.stackList.clear();
- this.stackList.addAll(fullStacks);
+ this.stackList.addAll(stacks);
notifyDataSetChanged();
}
} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/StackFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/StackFragment.java
index 726a74184..313e6e821 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/StackFragment.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/StackFragment.java
@@ -20,20 +20,27 @@ import java.util.List;
import it.niedermann.android.crosstabdnd.DragAndDropTab;
import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.databinding.FragmentStackBinding;
+import it.niedermann.nextcloud.deck.model.Card;
+import it.niedermann.nextcloud.deck.model.Stack;
import it.niedermann.nextcloud.deck.model.full.FullCard;
import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
+import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.WrappedLiveData;
import it.niedermann.nextcloud.deck.ui.MainViewModel;
import it.niedermann.nextcloud.deck.ui.branding.BrandedFragment;
import it.niedermann.nextcloud.deck.ui.card.CardAdapter;
import it.niedermann.nextcloud.deck.ui.card.SelectCardListener;
+import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment;
import it.niedermann.nextcloud.deck.ui.filter.FilterViewModel;
+import it.niedermann.nextcloud.deck.ui.movecard.MoveCardListener;
-public class StackFragment extends BrandedFragment implements DragAndDropTab<CardAdapter> {
+import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce;
+
+public class StackFragment extends BrandedFragment implements DragAndDropTab<CardAdapter>, MoveCardListener {
private static final String KEY_STACK_ID = "stackId";
private FragmentStackBinding binding;
- private SyncManager syncManager;
+ private MainViewModel mainViewModel;
private FragmentActivity activity;
private OnScrollListener onScrollListener;
@@ -61,10 +68,10 @@ public class StackFragment extends BrandedFragment implements DragAndDropTab<Car
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
- binding = FragmentStackBinding.inflate(inflater, container, false);
activity = requireActivity();
+ binding = FragmentStackBinding.inflate(inflater, container, false);
+ mainViewModel = new ViewModelProvider(activity).get(MainViewModel.class);
- final MainViewModel mainViewModel = new ViewModelProvider(activity).get(MainViewModel.class);
final FilterViewModel filterViewModel = new ViewModelProvider(activity).get(FilterViewModel.class);
// This might be a zombie fragment with an empty MainViewModel after Android killed the activity (but not the fragment instance
@@ -74,19 +81,10 @@ public class StackFragment extends BrandedFragment implements DragAndDropTab<Car
return binding.getRoot();
}
- syncManager = new SyncManager(activity);
-
- adapter = new CardAdapter(
- requireContext(),
- getChildFragmentManager(),
- mainViewModel.getCurrentAccount(),
- mainViewModel.getCurrentBoardLocalId(),
- mainViewModel.getCurrentBoardRemoteId(),
- stackId,
- mainViewModel.currentBoardHasEditPermission(),
- syncManager,
- this,
- (requireActivity() instanceof SelectCardListener) ? (SelectCardListener) requireActivity() : null);
+ adapter = new CardAdapter(requireContext(), getChildFragmentManager(), stackId, mainViewModel, this,
+ (requireActivity() instanceof SelectCardListener)
+ ? (SelectCardListener) requireActivity()
+ : null);
binding.recyclerView.setAdapter(adapter);
if (onScrollListener != null) {
@@ -114,12 +112,12 @@ public class StackFragment extends BrandedFragment implements DragAndDropTab<Car
}
});
- cardsLiveData = syncManager.getFullCardsForStack(mainViewModel.getCurrentAccount().getId(), stackId, filterViewModel.getFilterInformation().getValue());
+ cardsLiveData = mainViewModel.getFullCardsForStack(mainViewModel.getCurrentAccount().getId(), stackId, filterViewModel.getFilterInformation().getValue());
cardsLiveData.observe(getViewLifecycleOwner(), cardsObserver);
filterViewModel.getFilterInformation().observe(getViewLifecycleOwner(), (filterInformation -> {
cardsLiveData.removeObserver(cardsObserver);
- cardsLiveData = syncManager.getFullCardsForStack(mainViewModel.getCurrentAccount().getId(), stackId, filterInformation);
+ cardsLiveData = mainViewModel.getFullCardsForStack(mainViewModel.getCurrentAccount().getId(), stackId, filterInformation);
cardsLiveData.observe(getViewLifecycleOwner(), cardsObserver);
}));
@@ -153,4 +151,17 @@ public class StackFragment extends BrandedFragment implements DragAndDropTab<Car
return fragment;
}
+
+ @Override
+ public void move(long originAccountId, long originCardLocalId, long targetAccountId, long targetBoardLocalId, long targetStackLocalId) {
+ final WrappedLiveData<Void> liveData = mainViewModel.moveCard(originAccountId, originCardLocalId, targetAccountId, targetBoardLocalId, targetStackLocalId);
+ observeOnce(liveData, requireActivity(), (next) -> {
+ if (liveData.hasError() && !SyncManager.ignoreExceptionOnVoidError(liveData.getError())) {
+ ExceptionDialogFragment.newInstance(liveData.getError(), null).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName());
+ } else {
+ DeckLog.log("Moved " + Card.class.getSimpleName() + " \"" + originCardLocalId + "\" to " + Stack.class.getSimpleName() + " \"" + targetStackLocalId + "\"");
+ }
+ });
+ }
+
} \ No newline at end of file
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/ColorChooser.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/ColorChooser.java
index 30dc0ada4..0dd431ff9 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/ColorChooser.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/ColorChooser.java
@@ -2,6 +2,7 @@ package it.niedermann.nextcloud.deck.ui.view;
import android.content.Context;
import android.content.res.TypedArray;
+import android.graphics.Color;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
@@ -9,31 +10,31 @@ import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
+import androidx.annotation.ColorInt;
import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
import com.google.android.flexbox.FlexboxLayout;
import com.skydoves.colorpickerview.listeners.ColorEnvelopeListener;
+import java.util.Arrays;
+
+import it.niedermann.android.util.DimensionUtil;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.databinding.WidgetColorChooserBinding;
import it.niedermann.nextcloud.deck.util.ViewUtil;
-import static it.niedermann.nextcloud.deck.util.DimensionUtil.dpToPx;
-
public class ColorChooser extends LinearLayout {
- private WidgetColorChooserBinding binding;
-
- private final FlexboxLayout.LayoutParams params = new FlexboxLayout.LayoutParams(
- ViewGroup.LayoutParams.WRAP_CONTENT,
- ViewGroup.LayoutParams.WRAP_CONTENT
- );
+ private final WidgetColorChooserBinding binding;
- private Context context;
- private String[] colors;
+ private final Context context;
+ private final int[] colors;
- private String selectedColor;
- private String previouslySelectedColor;
+ @ColorInt
+ private int selectedColor;
+ @ColorInt
+ private int previouslySelectedColor;
@Nullable
private ImageView previouslySelectedImageView;
@@ -41,17 +42,22 @@ public class ColorChooser extends LinearLayout {
super(context, attrs);
this.context = context;
- params.setMargins(0, dpToPx(context, R.dimen.spacer_1x), 0, 0);
+ final FlexboxLayout.LayoutParams params = new FlexboxLayout.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT
+ );
+ params.setMargins(0, DimensionUtil.INSTANCE.dpToPx(context, R.dimen.spacer_1x), 0, 0);
params.setFlexBasisPercent(.15f);
- TypedArray a = context.obtainStyledAttributes(attrs,
- R.styleable.ColorChooser, 0, 0);
- colors = getResources().getStringArray(a.getResourceId(R.styleable.ColorChooser_colors, 0));
+ final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ColorChooser, 0, 0);
+ colors = Arrays.stream(getResources().getStringArray(a.getResourceId(R.styleable.ColorChooser_colors, 0)))
+ .mapToInt(Color::parseColor)
+ .toArray();
a.recycle();
binding = WidgetColorChooserBinding.inflate(LayoutInflater.from(context), this, true);
- for (final String color : colors) {
- ImageView image = new ImageView(getContext());
+ for (final int color : colors) {
+ final ImageView image = new ImageView(getContext());
image.setLayoutParams(params);
image.setOnClickListener((imageView) -> {
if (previouslySelectedImageView != null) { // null when first selection
@@ -61,7 +67,7 @@ public class ColorChooser extends LinearLayout {
selectedColor = color;
this.previouslySelectedColor = color;
this.previouslySelectedImageView = image;
- binding.customColorChooser.setImageDrawable(ViewUtil.getTintedImageView(this.context, R.drawable.circle_alpha_colorize_36dp, R.color.board_default_custom_color));
+ binding.customColorChooser.setImageDrawable(ViewUtil.getTintedImageView(this.context, R.drawable.circle_alpha_colorize_36dp, ContextCompat.getColor(context, R.color.board_default_custom_color)));
binding.customColorPicker.setVisibility(View.GONE);
binding.brightnessSlide.setVisibility(View.GONE);
});
@@ -84,19 +90,20 @@ public class ColorChooser extends LinearLayout {
previouslySelectedImageView.setImageDrawable(ViewUtil.getTintedImageView(this.context, R.drawable.circle_grey600_36dp, previouslySelectedColor));
previouslySelectedImageView = null;
}
- String customColor = "#" + envelope.getHexCode().substring(2);
+ @ColorInt
+ final int customColor = envelope.getColor();
selectedColor = customColor;
previouslySelectedColor = customColor;
binding.customColorChooser.setImageDrawable(ViewUtil.getTintedImageView(context, R.drawable.circle_alpha_colorize_36dp, selectedColor));
});
}
- public void selectColor(String newColor) {
+ public void selectColor(@ColorInt int newColor) {
boolean newColorIsCustomColor = true;
selectedColor = newColor;
for (int i = 0; i < colors.length; i++) {
- if (colors[i].equals(newColor)) {
- binding.customColorChooser.setImageDrawable(ViewUtil.getTintedImageView(this.context, R.drawable.circle_alpha_colorize_36dp, R.color.board_default_custom_color));
+ if (colors[i] == newColor) {
+ binding.customColorChooser.setImageDrawable(ViewUtil.getTintedImageView(this.context, R.drawable.circle_alpha_colorize_36dp, ContextCompat.getColor(context, R.color.board_default_custom_color)));
binding.colorPicker.getChildAt(i).performClick();
newColorIsCustomColor = false;
break;
@@ -107,7 +114,8 @@ public class ColorChooser extends LinearLayout {
}
}
- public String getSelectedColor() {
+ @ColorInt
+ public int getSelectedColor() {
return this.selectedColor;
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/OverlappingAvatars.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/OverlappingAvatars.java
index 501d33106..0facc385c 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/OverlappingAvatars.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/OverlappingAvatars.java
@@ -10,6 +10,7 @@ import android.widget.RelativeLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Px;
+import androidx.core.content.ContextCompat;
import androidx.core.graphics.drawable.DrawableCompat;
import com.bumptech.glide.Glide;
@@ -17,12 +18,11 @@ import com.bumptech.glide.request.RequestOptions;
import java.util.List;
+import it.niedermann.android.util.DimensionUtil;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.model.Account;
import it.niedermann.nextcloud.deck.model.User;
-import static it.niedermann.nextcloud.deck.util.DimensionUtil.dpToPx;
-
public class OverlappingAvatars extends RelativeLayout {
final int maxAvatarCount;
@Px
@@ -44,11 +44,12 @@ public class OverlappingAvatars extends RelativeLayout {
public OverlappingAvatars(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
maxAvatarCount = context.getResources().getInteger(R.integer.max_avatar_count);
- avatarBorderSize = dpToPx(context, R.dimen.avatar_size_small_overlapping_border);
- avatarSize = dpToPx(context, R.dimen.avatar_size_small) + avatarBorderSize * 2;
- overlapPx = dpToPx(context, R.dimen.avatar_size_small_overlapping);
- borderDrawable = getResources().getDrawable(R.drawable.avatar_border);
- DrawableCompat.setTint(borderDrawable, getResources().getColor(R.color.bg_card));
+ avatarBorderSize = DimensionUtil.INSTANCE.dpToPx(context, R.dimen.avatar_size_small_overlapping_border);
+ avatarSize = DimensionUtil.INSTANCE.dpToPx(context, R.dimen.avatar_size_small) + avatarBorderSize * 2;
+ overlapPx = DimensionUtil.INSTANCE.dpToPx(context, R.dimen.avatar_size_small_overlapping);
+ borderDrawable = ContextCompat.getDrawable(context, R.drawable.avatar_border);
+ assert borderDrawable != null;
+ DrawableCompat.setTint(borderDrawable, ContextCompat.getColor(context, R.color.bg_card));
}
public void setAvatars(@NonNull Account account, @NonNull List<User> assignedUsers) {
@@ -70,6 +71,7 @@ public class OverlappingAvatars extends RelativeLayout {
avatar.requestLayout();
Glide.with(context)
.load(account.getUrl() + "/index.php/avatar/" + Uri.encode(assignedUsers.get(avatarCount).getUid()) + "/" + avatarSize)
+ .placeholder(R.drawable.ic_person_grey600_24dp)
.error(R.drawable.ic_person_grey600_24dp)
.apply(RequestOptions.circleCropTransform())
.into(avatar);
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/ui/view/labelchip/CompactLabelChip.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labelchip/CompactLabelChip.java
new file mode 100644
index 000000000..a2a50430c
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labelchip/CompactLabelChip.java
@@ -0,0 +1,21 @@
+package it.niedermann.nextcloud.deck.ui.view.labelchip;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Px;
+
+import it.niedermann.android.util.DimensionUtil;
+import it.niedermann.nextcloud.deck.R;
+import it.niedermann.nextcloud.deck.model.Label;
+
+@SuppressLint("ViewConstructor")
+public class CompactLabelChip extends LabelChip {
+
+ public CompactLabelChip(@NonNull Context context, @NonNull Label label, @Px int gutter) {
+ super(context, label, gutter);
+ params.setFlexBasisPercent(1 / 6.5f);
+ setHeight(DimensionUtil.INSTANCE.dpToPx(context, R.dimen.compact_label_height));
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labelchip/DefaultLabelChip.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labelchip/DefaultLabelChip.java
new file mode 100644
index 000000000..80e44d7e0
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labelchip/DefaultLabelChip.java
@@ -0,0 +1,21 @@
+package it.niedermann.nextcloud.deck.ui.view.labelchip;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Px;
+
+import it.niedermann.nextcloud.deck.model.Label;
+
+import static android.text.TextUtils.TruncateAt.MIDDLE;
+
+@SuppressLint("ViewConstructor")
+public class DefaultLabelChip extends LabelChip {
+
+ public DefaultLabelChip(@NonNull Context context, @NonNull Label label, @Px int gutter) {
+ super(context, label, gutter);
+ setText(label.getTitle());
+ setEllipsize(MIDDLE);
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/LabelChip.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labelchip/LabelChip.java
index db4e123d3..83853f0b2 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/LabelChip.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labelchip/LabelChip.java
@@ -1,9 +1,8 @@
-package it.niedermann.nextcloud.deck.ui.view;
+package it.niedermann.nextcloud.deck.ui.view.labelchip;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.ColorStateList;
-import android.graphics.Color;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
@@ -12,26 +11,24 @@ import androidx.annotation.Px;
import com.google.android.flexbox.FlexboxLayout;
import com.google.android.material.chip.Chip;
+import it.niedermann.android.util.ColorUtil;
import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.model.Label;
-import it.niedermann.nextcloud.deck.util.ColorUtil;
-
-import static android.text.TextUtils.TruncateAt.MIDDLE;
@SuppressLint("ViewConstructor")
public class LabelChip extends Chip {
private final Label label;
+ protected final FlexboxLayout.LayoutParams params = new FlexboxLayout.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT
+ );
+
public LabelChip(@NonNull Context context, @NonNull Label label, @Px int gutter) {
super(context);
this.label = label;
- FlexboxLayout.LayoutParams params = new FlexboxLayout.LayoutParams(
- ViewGroup.LayoutParams.WRAP_CONTENT,
- ViewGroup.LayoutParams.WRAP_CONTENT
- );
-
params.setMargins(0, 0, gutter, 0);
setLayoutParams(params);
setEnsureMinTouchTargetSize(false);
@@ -42,15 +39,13 @@ public class LabelChip extends Chip {
setTextStartPadding(gutter);
setTextEndPadding(gutter);
setChipEndPadding(gutter);
-
- setText(label.getTitle());
- setEllipsize(MIDDLE);
+ setClickable(false);
try {
- int labelColor = Color.parseColor("#" + label.getColor());
+ int labelColor = label.getColor();
ColorStateList c = ColorStateList.valueOf(labelColor);
setChipBackgroundColor(c);
- setTextColor(ColorUtil.getForegroundColorForBackgroundColor(labelColor));
+ setTextColor(ColorUtil.INSTANCE.getForegroundColorForBackgroundColor(labelColor));
} catch (IllegalArgumentException e) {
DeckLog.logError(e);
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labellayout/CompactLabelLayout.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labellayout/CompactLabelLayout.java
new file mode 100644
index 000000000..1c5e35d97
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labellayout/CompactLabelLayout.java
@@ -0,0 +1,22 @@
+package it.niedermann.nextcloud.deck.ui.view.labellayout;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import androidx.annotation.NonNull;
+
+import it.niedermann.nextcloud.deck.model.Label;
+import it.niedermann.nextcloud.deck.ui.view.labelchip.CompactLabelChip;
+import it.niedermann.nextcloud.deck.ui.view.labelchip.LabelChip;
+
+public class CompactLabelLayout extends LabelLayout {
+
+ public CompactLabelLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected LabelChip createLabelChip(@NonNull Label label) {
+ return new CompactLabelChip(getContext(), label, gutter);
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labellayout/DefaultLabelLayout.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labellayout/DefaultLabelLayout.java
new file mode 100644
index 000000000..f2d6d0752
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labellayout/DefaultLabelLayout.java
@@ -0,0 +1,21 @@
+package it.niedermann.nextcloud.deck.ui.view.labellayout;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import androidx.annotation.NonNull;
+
+import it.niedermann.nextcloud.deck.model.Label;
+import it.niedermann.nextcloud.deck.ui.view.labelchip.DefaultLabelChip;
+
+public class DefaultLabelLayout extends LabelLayout {
+
+ public DefaultLabelLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected DefaultLabelChip createLabelChip(@NonNull Label label) {
+ return new DefaultLabelChip(getContext(), label, gutter);
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/LabelLayout.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labellayout/LabelLayout.java
index 814c63ce1..3db539e53 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/LabelLayout.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labellayout/LabelLayout.java
@@ -1,4 +1,4 @@
-package it.niedermann.nextcloud.deck.ui.view;
+package it.niedermann.nextcloud.deck.ui.view.labellayout;
import android.content.Context;
import android.util.AttributeSet;
@@ -11,21 +11,22 @@ import com.google.android.flexbox.FlexboxLayout;
import java.util.LinkedList;
import java.util.List;
+import it.niedermann.android.util.DimensionUtil;
import it.niedermann.nextcloud.deck.DeckLog;
import it.niedermann.nextcloud.deck.R;
import it.niedermann.nextcloud.deck.model.Label;
+import it.niedermann.nextcloud.deck.ui.view.labelchip.LabelChip;
-import static it.niedermann.nextcloud.deck.util.DimensionUtil.dpToPx;
-
-public class LabelLayout extends FlexboxLayout {
+public abstract class LabelLayout extends FlexboxLayout {
@Px
- private int gutter;
- private List<LabelChip> chipList = new LinkedList<>();
+ final protected int gutter;
+ @NonNull
+ final private List<LabelChip> chipList = new LinkedList<>();
public LabelLayout(Context context, AttributeSet attrs) {
super(context, attrs);
- this.gutter = dpToPx(context, R.dimen.spacer_1hx);
+ this.gutter = DimensionUtil.INSTANCE.dpToPx(context, R.dimen.spacer_1hx);
}
/**
@@ -82,9 +83,11 @@ public class LabelLayout extends FlexboxLayout {
continue labelList;
}
}
- LabelChip chip = new LabelChip(getContext(), label, gutter);
+ final LabelChip chip = createLabelChip(label);
addView(chip);
chipList.add(chip);
}
}
+
+ protected abstract LabelChip createLabelChip(@NonNull Label label);
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/singlecard/SelectCardForWidgetActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/singlecard/SelectCardForWidgetActivity.java
index 8c1fbe1fa..fe6e969e7 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/singlecard/SelectCardForWidgetActivity.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/singlecard/SelectCardForWidgetActivity.java
@@ -45,7 +45,7 @@ public class SelectCardForWidgetActivity extends MainActivity implements SelectC
@Override
public void onCardSelected(FullCard fullCard) {
- syncManager.addOrUpdateSingleCardWidget(appWidgetId, mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId(), fullCard.getLocalId());
+ mainViewModel.addOrUpdateSingleCardWidget(appWidgetId, mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId(), fullCard.getLocalId());
final Intent updateIntent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE, null,
getApplicationContext(), SingleCardWidget.class)
.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/singlecard/SingleCardWidget.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/singlecard/SingleCardWidget.java
index 783f98e00..b44b80d1b 100644
--- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/singlecard/SingleCardWidget.java
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/singlecard/SingleCardWidget.java
@@ -43,7 +43,7 @@ public class SingleCardWidget extends AppWidgetProvider {
views.setTextViewText(R.id.description, fullModel.getFullCard().getCard().getDescription());
if (fullModel.getFullCard().getCard().getDueDate() != null) {
- views.setTextViewText(R.id.card_due_date, DateUtil.getRelativeDateTimeString(context, fullModel.getFullCard().getCard().getDueDate().getTime()));
+ views.setTextViewText(R.id.card_due_date, DateUtil.getRelativeDateTimeString(context, fullModel.getFullCard().getCard().getDueDate().toEpochMilli()));
// TODO Use multiple views for background colors and only set the necessary to View.VISIBLE
// https://stackoverflow.com/a/3376537
// Because otherwise using Reflection is the only way
@@ -141,13 +141,10 @@ public class SingleCardWidget extends AppWidgetProvider {
super.onDeleted(context, appWidgetIds);
}
-
/**
* Updates UI data of all {@link SingleCardWidget} instances
*/
public static void notifyDatasetChanged(Context context) {
- Intent intent = new Intent(context, SingleCardWidget.class);
- intent.setAction("android.appwidget.action.APPWIDGET_UPDATE");
- context.sendBroadcast(intent);
+ context.sendBroadcast(new Intent(context, SingleCardWidget.class).setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE));
}
}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidget.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidget.java
new file mode 100644
index 000000000..cb179953c
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidget.java
@@ -0,0 +1,121 @@
+package it.niedermann.nextcloud.deck.ui.widget.stack;
+
+import android.app.PendingIntent;
+import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProvider;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.widget.RemoteViews;
+
+import java.util.NoSuchElementException;
+
+import it.niedermann.nextcloud.deck.R;
+import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.deck.model.appwidgets.StackWidgetModel;
+import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
+import it.niedermann.nextcloud.deck.ui.MainActivity;
+import it.niedermann.nextcloud.deck.ui.card.EditActivity;
+
+import static android.appwidget.AppWidgetManager.ACTION_APPWIDGET_UPDATE;
+
+public class StackWidget extends AppWidgetProvider {
+ public static final String ACCOUNT_ID_KEY = "stack_widget_account_id";
+ public static final String ACCOUNT_KEY = "stack_widget_account";
+ public static final String STACK_ID_KEY = "stack_widget_stack_id";
+ public static final String BUNDLE_KEY = "stack_widget_bundle";
+ private static final int PENDING_INTENT_OPEN_APP_RQ = 0;
+ private static final int PENDING_INTENT_EDIT_CARD_RQ = 1;
+
+ static void updateAppWidget(Context context, AppWidgetManager awm, int[] appWidgetIds, Account account) {
+ final SyncManager syncManager = new SyncManager(context);
+
+ for (int appWidgetId : appWidgetIds) {
+ new Thread(() -> {
+ try {
+ final StackWidgetModel model = syncManager.getStackWidgetModelDirectly(appWidgetId);
+ RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_stack);
+ Intent serviceIntent = new Intent(context, StackWidgetService.class);
+
+ serviceIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
+ serviceIntent.putExtra(ACCOUNT_ID_KEY + appWidgetId, model.getAccountId());
+ serviceIntent.putExtra(STACK_ID_KEY + appWidgetId, model.getStackId());
+ if (account != null) {
+ Bundle extras = new Bundle();
+ extras.putSerializable(StackWidget.ACCOUNT_KEY + appWidgetId, account);
+ serviceIntent.putExtra(BUNDLE_KEY + appWidgetId, extras);
+ }
+ serviceIntent.setData(Uri.parse(serviceIntent.toUri(Intent.URI_INTENT_SCHEME)));
+
+ Intent intent = new Intent(Intent.ACTION_MAIN);
+ intent.setComponent(new ComponentName(context.getPackageName(), MainActivity.class.getName()));
+ PendingIntent pendingIntent = PendingIntent.getActivity(context, PENDING_INTENT_OPEN_APP_RQ,
+ intent, PendingIntent.FLAG_UPDATE_CURRENT);
+ views.setOnClickPendingIntent(R.id.widget_stack_header_rl, pendingIntent);
+
+ PendingIntent templatePI = PendingIntent.getActivity(context, PENDING_INTENT_EDIT_CARD_RQ,
+ new Intent(context, EditActivity.class), PendingIntent.FLAG_UPDATE_CURRENT);
+
+ views.setPendingIntentTemplate(R.id.stack_widget_lv, templatePI);
+ views.setRemoteAdapter(R.id.stack_widget_lv, serviceIntent);
+ views.setEmptyView(R.id.stack_widget_lv, R.id.widget_stack_placeholder_iv);
+ awm.notifyAppWidgetViewDataChanged(appWidgetId, R.id.stack_widget_lv);
+ awm.updateAppWidget(appWidgetId, views);
+ } catch (NoSuchElementException e) {
+ // onUpdate has been triggered before the user finished configuring the widget
+ }
+ }).start();
+ }
+ }
+
+ @Override
+ public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
+ super.onUpdate(context, appWidgetManager, appWidgetIds);
+ updateAppWidget(context, appWidgetManager, appWidgetIds, null);
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final Account account;
+
+ super.onReceive(context, intent);
+
+ AppWidgetManager awm = AppWidgetManager.getInstance(context);
+
+ if (intent.getAction() != null) {
+ if (intent.getAction().equals(ACTION_APPWIDGET_UPDATE)) {
+ if (intent.hasExtra(BUNDLE_KEY)) {
+ Bundle extras = intent.getBundleExtra(StackWidget.BUNDLE_KEY);
+ account = (Account) extras.getSerializable(ACCOUNT_KEY);
+
+ if (intent.hasExtra(AppWidgetManager.EXTRA_APPWIDGET_ID)) {
+ if (intent.getExtras() != null) {
+ updateAppWidget(context, awm, new int[]{intent.getExtras().getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)}, account);
+ }
+ } else {
+ updateAppWidget(context, awm, awm.getAppWidgetIds(new ComponentName(context, StackWidget.class)), account);
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onDeleted(Context context, int[] appWidgetIds) {
+ super.onDeleted(context, appWidgetIds);
+ final SyncManager syncManager = new SyncManager(context);
+
+ for (int appWidgetId : appWidgetIds) {
+ syncManager.deleteStackWidgetModel(appWidgetId);
+ }
+ }
+
+ /**
+ * Updates UI data of all {@link StackWidget} instances
+ */
+ public static void notifyDatasetChanged(Context context) {
+ context.sendBroadcast(new Intent(context, StackWidget.class).setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE));
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetConfigurationActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetConfigurationActivity.java
new file mode 100644
index 000000000..96b5cf672
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetConfigurationActivity.java
@@ -0,0 +1,68 @@
+package it.niedermann.nextcloud.deck.ui.widget.stack;
+
+import android.appwidget.AppWidgetManager;
+import android.content.Intent;
+import android.os.Bundle;
+
+import androidx.appcompat.app.ActionBar;
+import androidx.lifecycle.ViewModelProvider;
+
+import it.niedermann.nextcloud.deck.DeckLog;
+import it.niedermann.nextcloud.deck.R;
+import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.deck.ui.PickStackActivity;
+
+public class StackWidgetConfigurationActivity extends PickStackActivity {
+ private int appWidgetId;
+ private StackWidgetConfigurationViewModel stackWidgetConfigurationViewModel;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ stackWidgetConfigurationViewModel = new ViewModelProvider(this).get(StackWidgetConfigurationViewModel.class);
+
+ final ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setTitle(R.string.add_stack_widget);
+ }
+
+ setResult(RESULT_CANCELED);
+ final Bundle extras = getIntent().getExtras();
+
+ if (extras != null) {
+ appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID,
+ AppWidgetManager.INVALID_APPWIDGET_ID);
+ }
+
+ if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
+ DeckLog.error("INVALID_APPWIDGET_ID");
+ finish();
+ }
+ }
+
+ @Override
+ protected void onSubmit(Account account, long boardId, long stackId) {
+ final Bundle extras = new Bundle();
+
+ stackWidgetConfigurationViewModel.addStackWidget(appWidgetId, account.getId(), stackId, false);
+ Intent updateIntent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE, null,
+ getApplicationContext(), StackWidget.class);
+ extras.putSerializable(StackWidget.ACCOUNT_KEY, account);
+ extras.putInt(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
+
+ // The `extras` bundle is added to the intent this way because using putExtras(extras)
+ // would have the OS attempt to reassemle the data and cause a crash
+ // when it finds classes that are only known to this application.
+ updateIntent.putExtra(StackWidget.BUNDLE_KEY, extras);
+ setResult(RESULT_OK, updateIntent);
+ getApplicationContext().sendBroadcast(updateIntent);
+
+ finish();
+ }
+
+ @Override
+ protected boolean showBoardsWithoutEditPermission() {
+ return true;
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetConfigurationViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetConfigurationViewModel.java
new file mode 100644
index 000000000..cc669accf
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetConfigurationViewModel.java
@@ -0,0 +1,23 @@
+package it.niedermann.nextcloud.deck.ui.widget.stack;
+
+import android.app.Application;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.AndroidViewModel;
+
+import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
+
+@SuppressWarnings("WeakerAccess")
+public class StackWidgetConfigurationViewModel extends AndroidViewModel {
+
+ private final SyncManager syncManager;
+
+ public StackWidgetConfigurationViewModel(@NonNull Application application) {
+ super(application);
+ this.syncManager = new SyncManager(application);
+ }
+
+ public void addStackWidget(int appWidgetId, long accountId, long stackId, boolean darkTheme) {
+ syncManager.addStackWidget(appWidgetId, accountId, stackId, darkTheme);
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetFactory.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetFactory.java
new file mode 100644
index 000000000..87f357e20
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetFactory.java
@@ -0,0 +1,134 @@
+package it.niedermann.nextcloud.deck.ui.widget.stack;
+
+import android.appwidget.AppWidgetManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.widget.RemoteViews;
+import android.widget.RemoteViewsService;
+
+import androidx.lifecycle.LiveData;
+
+import java.util.List;
+
+import it.niedermann.nextcloud.deck.DeckLog;
+import it.niedermann.nextcloud.deck.R;
+import it.niedermann.nextcloud.deck.model.Account;
+import it.niedermann.nextcloud.deck.model.full.FullBoard;
+import it.niedermann.nextcloud.deck.model.full.FullCard;
+import it.niedermann.nextcloud.deck.model.full.FullStack;
+import it.niedermann.nextcloud.deck.persistence.sync.SyncManager;
+import it.niedermann.nextcloud.deck.ui.card.EditActivity;
+
+public class StackWidgetFactory implements RemoteViewsService.RemoteViewsFactory {
+ private final Context context;
+ private final int appWidgetId;
+ private final long accountId;
+ private final long stackId;
+
+ private Account account;
+ private FullStack stack;
+ private List<FullCard> cardList;
+
+ StackWidgetFactory(Context context, Intent intent) {
+ this.context = context;
+ appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
+ AppWidgetManager.INVALID_APPWIDGET_ID);
+ accountId = intent.getLongExtra(StackWidget.ACCOUNT_ID_KEY + appWidgetId, -1);
+ stackId = intent.getLongExtra(StackWidget.STACK_ID_KEY + appWidgetId, -1);
+ if (intent.hasExtra(StackWidget.BUNDLE_KEY + appWidgetId)) {
+ account = (Account) intent.getBundleExtra(StackWidget.BUNDLE_KEY + appWidgetId).getSerializable(StackWidget.ACCOUNT_KEY + appWidgetId);
+ }
+ }
+
+ @Override
+ public void onCreate() {
+ SyncManager syncManager = new SyncManager(context);
+
+ LiveData<FullStack> stackLiveData = syncManager.getStack(accountId, stackId);
+ stackLiveData.observeForever((FullStack fullStack) -> {
+ if (fullStack != null) {
+ RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_stack);
+ stack = fullStack;
+ views.setTextViewText(R.id.widget_stack_title_tv, stack.getStack().getTitle());
+
+ LiveData<FullBoard> fullBoardLiveData = syncManager.getFullBoardById(accountId, stack.getStack().getBoardId());
+ fullBoardLiveData.observeForever((FullBoard fullBoard) -> {
+ if (fullBoard != null) {
+ views.setInt(R.id.widget_stack_header_icon, "setColorFilter", fullBoard.getBoard().getColor());
+ notifyAppWidgetUpdate(views);
+ }
+ });
+
+ LiveData<List<FullCard>> fullCardData = syncManager.getFullCardsForStack(accountId, stackId, null);
+ fullCardData.observeForever((List<FullCard> fullCards) -> cardList = fullCards);
+ notifyAppWidgetUpdate(views);
+ }
+ });
+ }
+
+ @Override
+ public void onDataSetChanged() {
+
+ }
+
+
+ @Override
+ public void onDestroy() {
+
+ }
+
+ @Override
+ public int getCount() {
+ return stack == null ? 0 : stack.getCards().size();
+ }
+
+ @Override
+ public RemoteViews getViewAt(int i) {
+ RemoteViews widget_entry;
+
+ if (cardList == null || i > (cardList.size() - 1) || cardList.get(i) == null) {
+ DeckLog.error("Card not found at position " + i);
+ return null;
+ }
+
+ FullCard card = cardList.get(i);
+
+ widget_entry = new RemoteViews(context.getPackageName(), R.layout.widget_stack_entry);
+ widget_entry.setTextViewText(R.id.widget_entry_content_tv, card.card.getTitle());
+
+ final Intent intent = EditActivity.createEditCardIntent(context, account, stack.getStack().getBoardId(), card.getCard().getLocalId());
+ intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
+ widget_entry.setOnClickFillInIntent(R.id.widget_stack_entry, intent);
+
+ return widget_entry;
+ }
+
+ @Override
+ public RemoteViews getLoadingView() {
+ return null;
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return 1;
+ }
+
+ @Override
+ public long getItemId(int i) {
+ return i;
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ private void notifyAppWidgetUpdate(RemoteViews views) {
+ AppWidgetManager awm = AppWidgetManager.getInstance(context);
+ int[] appWidgetIds = awm.getAppWidgetIds(new ComponentName(context, StackWidget.class));
+ awm.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.stack_widget_lv);
+ awm.updateAppWidget(appWidgetId, views);
+ }
+}
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetService.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetService.java
new file mode 100644
index 000000000..9299a96e2
--- /dev/null
+++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetService.java
@@ -0,0 +1,11 @@
+package it.niedermann.nextcloud.deck.ui.widget.stack;
+
+import android.content.Intent;
+import android.widget.RemoteViewsService;
+
+public class StackWidgetService extends RemoteViewsService {
+ @Override
+ public RemoteViewsFactory onGetViewFactory(Intent intent) {
+ return new StackWidgetFactory(this.getApplicationContext(), intent);
+ }
+}