diff options
author | Stefan Niedermann <info@niedermann.it> | 2020-12-09 19:59:07 +0300 |
---|---|---|
committer | Stefan Niedermann <info@niedermann.it> | 2020-12-09 19:59:07 +0300 |
commit | df900e53492c7b30cbb90b9180d6c3cdf59f38d9 (patch) | |
tree | b88dc0386e6b448c95cf4fcb46fbeaf8edc8780f /app/src/main/java/it/niedermann/nextcloud/deck/ui | |
parent | 034ae108ae4ab4c273ef4d74f1bfd39fbc4d8a84 (diff) | |
parent | f29eed9db4c0906fa7887e446cf0325718ef6827 (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')
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); + } +} |