diff options
76 files changed, 1577 insertions, 319 deletions
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index c93519c29..69da32e6d 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -8,7 +8,7 @@ labels: bug Guidelines for submitting bug reports: * Bug reports which do not fill the complete issue template will be closed. -* Please have a look at the [FAQ in our wiki](https://github.com/stefan-niedermann/nextcloud-deck/wiki/FAQ) +* Please have a look at our [FAQ](https://github.com/stefan-niedermann/nextcloud-deck/blob/master/FAQ.md) * Please search the existing issues first, it's likely that your issue was already reported or even fixed. * This repository is *only* for issues within the Nextcloud Deck Android app --> diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..f862478d0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: 💼 Volume licenses + url: https://www.niedermann.it/kontakt.html + about: If you are a company, a club, a university or another organization, please contact us directly for volume licenses.
\ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 95c06ceff..f10fbeb95 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -8,7 +8,7 @@ labels: enhancement Guidelines for submitting issues: * Please search the existing issues first, it's likely that your issue was already reported or even fixed. -* This repository is *only* for issues within the Nextcloud Notes Android app +* This repository is *only* for issues within the Nextcloud Deck Android app --> <!-- Please keep this note for other contributors --> @@ -0,0 +1,51 @@ +# Frequently asked questions + +## I have experienced an error + +Sorry. There are so many different environments, that it is impossible for us to test each and every constellation. + +First of all make sure you have updated to and tried with the latest available versions of both, this app and the [Deck server app](https://apps.nextcloud.com/apps/deck). + +In case you receive a `NextcloudApiNotRespondingException`, try to disable the battery optimization for both apps. +In all other cases please try to clear the storage of **both** apps, Nextcloud Android **and** Nextcloud Deck Android. + +You can achieve this by navigating to + +``` +Android settings + ↳ Apps + ↳ Nextcloud / Deck + ↳ Storage + ↳ Clear storage +``` + +Often there is an issue with the state of the server app. Try to create a new test account at your Nextcloud instance and connect to it. +If there are no errors, share the boards of your actual account one by one with your test account to find the defect one. + +If the issue persists, [open a bug report in our issue tracker](https://github.com/stefan-niedermann/nextcloud-deck/issues/new?assignees=&labels=bug&template=bug_report.md&title=). + +## Why has my bug report been closed? + +As stated in the bug templates, we reserve to close issues which do not fill the **complete** issue template. The information we ask for is urgently needed, even if it might not seem to be important or relevant to you. + +We have very limited resources and capacity and we really want to help you fixing different bugs, but we can impossibly know your environment, your different software versions, the store you used. +Therefore it is extremely important for you to describe the **exact steps to reproduce**. This includes information about your environment. + +Example for a bad description: + +> 1. The app crashes when i save a card + +Example for a good description: + +> 1. Open any existing card\ +> 2. Add text to the description\ +> 3. Click on the ✕ in the top left\ +> 4. Answer "Save" when asking to discard or save the changes\ +> 5. See app crash + +We also preserve to close issues where the original reporter does not answer within a certain time frame. We usually answer issues within a hour and expect you to respond to our questions within a week. + +This is necessary for two reasons: + +1. We have a rapid development cycle - bugs which have been reported weeks ago might no longer relevant +2. We are loosing the context of a report or a question over the time. We have many things to care about and digging into an issue deep and then relying on an response which is not coming is a waste of our limited free time
\ No newline at end of file diff --git a/PRIVACY.md b/PRIVACY.md index 35e8083f0..8dbaa93de 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -9,22 +9,10 @@ The license of this project allows you to verify that no data is collected by th This is a list of permissions required and asked by the App in order to properly work on your device: -- `com.nextcloud.android.sso` - - Used to connect to the [main Nextcloud Android app](https://github.com/nextcloud/android/) via the [Nextcloud Single Sign On](https://github.com/nextcloud/Android-SingleSignOn/) mechanism. This is required not only to import an account but also to synchronize the account with the Nextcloud instance. - - `android.permission.ACCESS_NETWORK_STATE` Used to provide offline support and make the "Sync only on Wi-Fi" option possible. -- `android.permission.READ_EXTERNAL_STORAGE` - - Used for reading files to attach them to cards. - -- `android.permission.WRITE_EXTERNAL_STORAGE` - - Used for caching attachments and other files on the disk. - - `android.permission.INTERNET` Used by [Nextcloud Single Sign On library](https://github.com/nextcloud/Android-SingleSignOn/) to communicate with your Nextcloud instance and synchronize contents. diff --git a/app/build.gradle b/app/build.gradle index 92c79b7cf..ce2b57e48 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,8 +6,8 @@ android { applicationId "it.niedermann.nextcloud.deck" minSdkVersion 17 targetSdkVersion 29 - versionCode 1006000 - versionName "1.6.0" + versionCode 1008000 + versionName "1.8.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true javaCompileOptions { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4b62c8cc5..0d9268e49 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,10 +3,7 @@ xmlns:tools="http://schemas.android.com/tools" package="it.niedermann.nextcloud.deck"> - <uses-permission android:name="com.nextcloud.android.sso" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> - <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> - <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <application android:name="it.niedermann.nextcloud.deck.DeckApplication" diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/api/GsonConfig.java b/app/src/main/java/it/niedermann/nextcloud/deck/api/GsonConfig.java index b9f6f403d..7c1727fb7 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/api/GsonConfig.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/api/GsonConfig.java @@ -40,6 +40,7 @@ public class GsonConfig { Type capabilities = new TypeToken<Capabilities>() {}.getType(); Type ocsUserList = new TypeToken<OcsUserList>() {}.getType(); Type activity = new TypeToken<Activity>() {}.getType(); + Type activityList = new TypeToken<List<Activity>>() {}.getType(); Type attachment = new TypeToken<Attachment>() {}.getType(); Type attachmentList = new TypeToken<List<Attachment>>() {}.getType(); Type comment = new TypeToken<OcsComment>() {}.getType(); @@ -59,6 +60,7 @@ public class GsonConfig { .registerTypeAdapter(capabilities, new NextcloudDeserializer<>("capability", Capabilities.class)) .registerTypeAdapter(ocsUserList, new NextcloudDeserializer<>("ocsUserList", OcsUserList.class)) .registerTypeAdapter(activity, new NextcloudDeserializer<>("activity", Activity.class)) + .registerTypeAdapter(activityList, new NextcloudDeserializer<>("activityList", Activity.class)) .registerTypeAdapter(attachmentList, new NextcloudArrayDeserializer<>("attachments", Attachment.class)) .registerTypeAdapter(attachment, new NextcloudDeserializer<>("attachment", Attachment.class)) .registerTypeAdapter(comment, new NextcloudDeserializer<>("comment", OcsComment.class)) diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/api/JsonToEntityParser.java b/app/src/main/java/it/niedermann/nextcloud/deck/api/JsonToEntityParser.java index 2bd7764e9..578104bba 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/api/JsonToEntityParser.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/api/JsonToEntityParser.java @@ -14,7 +14,6 @@ import java.util.List; import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.exceptions.DeckException; -import it.niedermann.nextcloud.deck.exceptions.TraceableException; import it.niedermann.nextcloud.deck.model.AccessControl; import it.niedermann.nextcloud.deck.model.Attachment; import it.niedermann.nextcloud.deck.model.Board; @@ -36,6 +35,7 @@ import it.niedermann.nextcloud.deck.model.ocs.user.OcsUser; import it.niedermann.nextcloud.deck.model.ocs.user.OcsUserList; import static it.niedermann.nextcloud.deck.exceptions.DeckException.Hint.CAPABILITIES_VERSION_NOT_PARSABLE; +import static it.niedermann.nextcloud.deck.exceptions.TraceableException.makeTraceableIfFails; public class JsonToEntityParser { @@ -65,7 +65,7 @@ public class JsonToEntityParser { private static OcsUserList parseOcsUserList(JsonObject obj) { DeckLog.verbose(obj.toString()); OcsUserList ocsUserList = new OcsUserList(); - TraceableException.makeTraceableIfFails(() -> { + makeTraceableIfFails(() -> { JsonElement data = obj.get("ocs").getAsJsonObject().get("data"); if (!data.isJsonNull() && data.getAsJsonObject().has("users")) { JsonElement users = data.getAsJsonObject().get("users"); @@ -90,7 +90,7 @@ public class JsonToEntityParser { private static OcsComment parseOcsComment(JsonObject obj) { DeckLog.verbose(obj.toString()); OcsComment comment = new OcsComment(); - TraceableException.makeTraceableIfFails(() -> { + makeTraceableIfFails(() -> { JsonElement data = obj.get("ocs").getAsJsonObject().get("data"); if (data.isJsonArray()) { for (JsonElement deckComment : data.getAsJsonArray()) { @@ -107,7 +107,7 @@ public class JsonToEntityParser { DeckLog.verbose(data.toString()); DeckComment deckComment = new DeckComment(); - TraceableException.makeTraceableIfFails(() -> { + makeTraceableIfFails(() -> { JsonObject commentJson = data.getAsJsonObject(); deckComment.setId(commentJson.get("id").getAsLong()); @@ -138,7 +138,7 @@ public class JsonToEntityParser { DeckLog.verbose(mentionJson.toString()); - TraceableException.makeTraceableIfFails(() -> { + makeTraceableIfFails(() -> { JsonObject mentionObject = mentionJson.getAsJsonObject(); mention.setMentionId(mentionObject.get("mentionId").getAsString()); mention.setMentionType(mentionObject.get("mentionType").getAsString()); @@ -155,7 +155,7 @@ public class JsonToEntityParser { DeckLog.verbose(e.toString()); Board board = new Board(); - TraceableException.makeTraceableIfFails(() -> { + makeTraceableIfFails(() -> { board.setTitle(getNullAsEmptyString(e.get("title"))); board.setColor(getNullAsEmptyString(e.get("color"))); board.setArchived(e.get("archived").getAsBoolean()); @@ -234,7 +234,7 @@ public class JsonToEntityParser { AccessControl acl = new AccessControl(); if (aclJson.has("participant") && !aclJson.get("participant").isJsonNull()) { - TraceableException.makeTraceableIfFails(() -> { + makeTraceableIfFails(() -> { User participant = parseUser(aclJson.get("participant").getAsJsonObject()); acl.setUser(participant); acl.setType(aclJson.get("type").getAsLong()); @@ -257,7 +257,7 @@ public class JsonToEntityParser { FullCard fullCard = new FullCard(); Card card = new Card(); fullCard.setCard(card); - TraceableException.makeTraceableIfFails(() -> { + makeTraceableIfFails(() -> { card.setId(e.get("id").getAsLong()); card.setTitle(getNullAsEmptyString(e.get("title"))); card.setDescription(getNullAsEmptyString(e.get("description"))); @@ -322,7 +322,7 @@ public class JsonToEntityParser { protected static Attachment parseAttachment(JsonObject e) { DeckLog.verbose(e.toString()); Attachment a = new Attachment(); - TraceableException.makeTraceableIfFails(() -> { + makeTraceableIfFails(() -> { a.setId(e.get("id").getAsLong()); a.setCardId(e.get("cardId").getAsLong()); a.setType(e.get("type").getAsString()); @@ -353,7 +353,7 @@ public class JsonToEntityParser { protected static User parseUser(JsonObject e) { DeckLog.verbose(e.toString()); User user = new User(); - TraceableException.makeTraceableIfFails(() -> { + makeTraceableIfFails(() -> { user.setDisplayname(getNullAsEmptyString(e.get("displayname"))); user.setPrimaryKey(getNullAsEmptyString(e.get("primaryKey"))); user.setUid(getNullAsEmptyString(e.get("uid"))); @@ -419,7 +419,7 @@ public class JsonToEntityParser { DeckLog.verbose(e.toString()); List<Activity> activityList = new ArrayList<>(); - TraceableException.makeTraceableIfFails(() -> { + makeTraceableIfFails(() -> { if (e.has("ocs")) { JsonObject ocs = e.getAsJsonObject("ocs"); if (ocs.has("data")) { @@ -447,7 +447,7 @@ public class JsonToEntityParser { FullStack fullStack = new FullStack(); Stack stack = new Stack(); fullStack.setStack(stack); - TraceableException.makeTraceableIfFails(() -> { + makeTraceableIfFails(() -> { stack.setTitle(getNullAsEmptyString(e.get("title"))); stack.setBoardId(e.get("boardId").getAsLong()); stack.setId(e.get("id").getAsLong()); @@ -474,7 +474,7 @@ public class JsonToEntityParser { protected static Label parseLabel(JsonObject e) { DeckLog.verbose(e.toString()); Label label = new Label(); - TraceableException.makeTraceableIfFails(() -> { + makeTraceableIfFails(() -> { label.setId(e.get("id").getAsLong()); //todo: last modified! // label.setLastModified(get); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/exceptions/TraceableException.java b/app/src/main/java/it/niedermann/nextcloud/deck/exceptions/TraceableException.java index a53ce8c1f..7acfb60bd 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/exceptions/TraceableException.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/exceptions/TraceableException.java @@ -12,21 +12,21 @@ public class TraceableException extends RuntimeException { try { runnable.run(); } catch (Throwable t) { - String message = "Sorry, a wild error appeared!\n" + - "### If you want to tell us about the following issue, " + - "please make sure to censor sensitive data beforehand! ###\n" + - "Failed to run traceable code"; + final StringBuilder message = new StringBuilder("Sorry, a wild error appeared!\n\n" + + "⚠️ If you want to tell us about the following issue, " + + "please make sure to censor sensitive data beforehand! ⚠️\n\n" + + "Failed to run traceable code"); if (args != null && args.length > 0) { - message += " with arguments:\n"; + message.append(" with arguments:\n"); for (Object arg : args) { - message += (arg == null ? "null" : arg.toString())+"\n"; + message.append(arg == null ? "null" : arg.toString()).append("\n"); } } else { - message += ":\n"; + message.append(":\n"); } - message += "Cause: " + t.getLocalizedMessage(); - TraceableException ex = new TraceableException(message, t); + message.append("Cause: ").append(t.getLocalizedMessage()); + TraceableException ex = new TraceableException(message.toString(), t); DeckLog.logError(ex); throw ex; } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/internal/FilterInformation.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/internal/FilterInformation.java index 54fb192ff..a10097c36 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/model/internal/FilterInformation.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/internal/FilterInformation.java @@ -77,7 +77,7 @@ public class FilterInformation implements Serializable { } /** - * @return whether or not the given filterInformation has any actual filters set + * @return whether or not the given {@param filterInformation} has any actual filters set */ public static boolean hasActiveFilter(@Nullable FilterInformation filterInformation) { if (filterInformation == null) { diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/SyncManager.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/SyncManager.java index d5b497683..850bdf9dc 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/SyncManager.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/SyncManager.java @@ -21,6 +21,8 @@ import java.util.Date; import java.util.List; import java.util.NoSuchElementException; import java.util.concurrent.CountDownLatch; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.api.GsonConfig; @@ -47,13 +49,10 @@ import it.niedermann.nextcloud.deck.model.ocs.Capabilities; import it.niedermann.nextcloud.deck.model.ocs.comment.DeckComment; import it.niedermann.nextcloud.deck.model.ocs.comment.OcsComment; import it.niedermann.nextcloud.deck.model.ocs.comment.full.FullDeckComment; -import it.niedermann.nextcloud.deck.model.ocs.user.OcsUser; -import it.niedermann.nextcloud.deck.model.ocs.user.OcsUserList; import it.niedermann.nextcloud.deck.persistence.sync.adapters.ServerAdapter; import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.DataBaseAdapter; 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.persistence.sync.adapters.db.util.extrawurst.Debouncer; import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.extrawurst.UserSearchLiveData; import it.niedermann.nextcloud.deck.persistence.sync.helpers.DataPropagationHelper; import it.niedermann.nextcloud.deck.persistence.sync.helpers.SyncHelper; @@ -67,7 +66,8 @@ import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.CardPropa import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.DeckCommentsDataProvider; import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.LabelDataProvider; import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.StackDataProvider; -import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.partial.BoardWitAclDownSyncDataProvider; +import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.partial.BoardWithAclDownSyncDataProvider; +import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.partial.BoardWithStacksAndLabelsUpSyncDataProvider; import it.niedermann.nextcloud.deck.util.DateUtil; import static java.net.HttpURLConnection.HTTP_UNAVAILABLE; @@ -336,7 +336,7 @@ public class SyncManager { public void onResponse(Boolean response) { liveData.postValue(dataBaseAdapter.readAccountsForHostWithReadAccessToBoardDirectly(host, boardRemoteId)); } - }).doSyncFor(new BoardWitAclDownSyncDataProvider()); + }).doSyncFor(new BoardWithAclDownSyncDataProvider()); } }); }); @@ -452,21 +452,43 @@ public class SyncManager { * Does <strong>not</strong> clone any {@link Card} or {@link AccessControl} from the origin {@link Board}. */ @AnyThread - public WrappedLiveData<FullBoard> cloneBoard(long originAccountId, long originBoardLocalId, long targetAccountId, String targetBoardTitle, String targetBoardColor) { + public WrappedLiveData<FullBoard> cloneBoard(long originAccountId, long originBoardLocalId, long targetAccountId, String targetBoardColor) { WrappedLiveData<FullBoard> liveData = new WrappedLiveData<>(); doAsync(() -> { Account originAccount = dataBaseAdapter.getAccountByIdDirectly(originAccountId); User newOwner = dataBaseAdapter.getUserByUidDirectly(originAccountId, originAccount.getUserName()); FullBoard originalBoard = dataBaseAdapter.getFullBoardByLocalIdDirectly(originAccountId, originBoardLocalId); + String newBoardTitleBaseName = originalBoard.getBoard().getTitle().trim(); + int newBoardTitleCopyIndex = 0; + //already a copy? + String regex = " \\(copy [0-9]+\\)$"; + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(originalBoard.getBoard().getTitle()); + if (matcher.find()) { + String found = matcher.group(); + newBoardTitleBaseName = newBoardTitleBaseName.substring(0, newBoardTitleBaseName.length() - found.length()); + Matcher indexMatcher = Pattern.compile("[0-9]+").matcher(found); + indexMatcher.find(); + String oldIndexString = indexMatcher.group(); + newBoardTitleCopyIndex = Integer.parseInt(oldIndexString); + } + + String newBoardTitle; + do { + newBoardTitleCopyIndex++; + newBoardTitle = newBoardTitleBaseName + " (copy " + newBoardTitleCopyIndex + ")"; + + } while (dataBaseAdapter.getBoardForAccountByNameDirectly(targetAccountId, newBoardTitle) != null); + originalBoard.setAccountId(targetAccountId); - originalBoard.getBoard().setTitle(targetBoardTitle); + originalBoard.setId(null); + originalBoard.setLocalId(null); + originalBoard.getBoard().setTitle(newBoardTitle); originalBoard.getBoard().setColor(targetBoardColor); - originalBoard.getBoard().setOwnerId(newOwner.getId()); + originalBoard.getBoard().setOwnerId(newOwner.getLocalId()); originalBoard.setStatusEnum(DBStatus.LOCAL_EDITED); originalBoard.setOwner(newOwner); - originalBoard.setId(null); - originalBoard.setLocalId(null); long newBoardId = dataBaseAdapter.createBoardDirectly(originAccountId, originalBoard.getBoard()); originalBoard.setLocalId(newBoardId); @@ -478,6 +500,7 @@ public class SyncManager { stack.setBoardId(newBoardId); dataBaseAdapter.createStack(targetAccountId, stack); } + originalBoard.setStacks(null); for (Label label : originalBoard.getLabels()) { label.setLocalId(null); label.setId(null); @@ -486,21 +509,28 @@ public class SyncManager { label.setBoardId(newBoardId); dataBaseAdapter.createLabel(targetAccountId, label); } - Account targetAccount = dataBaseAdapter.getAccountByIdDirectly(targetAccountId); - new SyncHelper(serverAdapter, dataBaseAdapter, null) - .setResponseCallback(new IResponseCallback<Boolean>(targetAccount) { - @Override - public void onResponse(Boolean response) { - liveData.postValue(dataBaseAdapter.getFullBoardByLocalIdDirectly(targetAccountId, newBoardId)); - } - - @Override - public void onError(Throwable throwable) { - super.onError(throwable); - liveData.postError(throwable); - } - }).doSyncFor(new BoardDataProvider()); + if (serverAdapter.hasInternetConnection()) { + Account targetAccount = dataBaseAdapter.getAccountByIdDirectly(targetAccountId); + ServerAdapter serverAdapterToUse = this.serverAdapter; + if (originAccountId != targetAccountId) { + serverAdapterToUse = new ServerAdapter(appContext, targetAccount.getName()); + } + new SyncHelper(serverAdapterToUse, dataBaseAdapter, null) + .setResponseCallback(new IResponseCallback<Boolean>(targetAccount) { + @Override + public void onResponse(Boolean response) { + liveData.postValue(dataBaseAdapter.getFullBoardByLocalIdDirectly(targetAccountId, newBoardId)); + } + @Override + public void onError(Throwable throwable) { + super.onError(throwable); + liveData.postError(throwable); + } + }).doUpSyncFor(new BoardWithStacksAndLabelsUpSyncDataProvider(originalBoard)); + } else { + liveData.postValue(dataBaseAdapter.getFullBoardByLocalIdDirectly(targetAccountId, newBoardId)); + } }); return liveData; } @@ -626,8 +656,7 @@ public class SyncManager { new AccessControlDataProvider(null, board, Collections.singletonList(entity)), entity, getCallbackToLiveDataConverter(account, liveData), ((entity1, response) -> { response.setBoardId(entity.getBoardId()); response.setUserId(entity.getUser().getLocalId()); - } - ) + }) ); }); return liveData; @@ -892,7 +921,7 @@ public class SyncManager { doAsync(() -> { FullCard fullCard = dataBaseAdapter.getFullCardByLocalIdDirectly(card.getAccountId(), card.getLocalId()); if (fullCard == null) { - throw new IllegalArgumentException("card to delete does not exist."); + throw new IllegalArgumentException("card with id " + card.getLocalId() + " to delete does not exist."); } Account account = dataBaseAdapter.getAccountByIdDirectly(card.getAccountId()); FullStack stack = dataBaseAdapter.getFullStackByLocalIdDirectly(card.getStackId()); @@ -970,6 +999,7 @@ public class SyncManager { return liveData; } + // TODO return WrappedLiveData for error handling @AnyThread public void archiveBoard(@NonNull Board board) { doAsync(() -> { @@ -979,6 +1009,7 @@ public class SyncManager { }); } + // TODO return WrappedLiveData for error handling @AnyThread public void dearchiveBoard(@NonNull Board board) { doAsync(() -> { @@ -1077,7 +1108,7 @@ public class SyncManager { } // ### get rid of original card where it is now. Card originalInnerCard = originalCard.getCard(); - deleteCard(originalInnerCard); + deleteCard(new Card(originalInnerCard)); // ### clone card itself Card targetCard = originalInnerCard; targetCard.setAccountId(targetAccountId); @@ -1086,7 +1117,9 @@ public class SyncManager { targetCard.setStatusEnum(DBStatus.LOCAL_EDITED); targetCard.setStackId(targetStackLocalId); targetCard.setOrder(newIndex); - //TODO: this needs to propagate to server as well, since anything else propagates as well (otherwise card isn't known on server) + targetCard.setArchived(false); + targetCard.setAttachmentCount(0); + targetCard.setCommentsUnread(0); FullCard fullCardForServerPropagation = new FullCard(); fullCardForServerPropagation.setCard(targetCard); @@ -1095,7 +1128,11 @@ public class SyncManager { FullStack targetFullStack = dataBaseAdapter.getFullStackByLocalIdDirectly(targetStackLocalId); User userOfTargetAccount = dataBaseAdapter.getUserByUidDirectly(targetAccountId, targetAccount.getUserName()); CountDownLatch latch = new CountDownLatch(1); - new DataPropagationHelper(serverAdapter, dataBaseAdapter).createEntity(new CardPropagationDataProvider(null, targetBoard.getBoard(), targetFullStack), fullCardForServerPropagation, new IResponseCallback<FullCard>(targetAccount) { + ServerAdapter serverToUse = serverAdapter; + if (originAccountId != targetAccountId) { + serverToUse = new ServerAdapter(appContext, targetAccount.getName()); + } + new DataPropagationHelper(serverToUse, dataBaseAdapter).createEntity(new CardPropagationDataProvider(null, targetBoard.getBoard(), targetFullStack), fullCardForServerPropagation, new IResponseCallback<FullCard>(targetAccount) { @Override public void onResponse(FullCard response) { targetCard.setId(response.getId()); @@ -1109,8 +1146,10 @@ public class SyncManager { throw new RuntimeException("unable to create card in moveCard target", throwable); } }, (FullCard entity, FullCard response) -> { - response.getCard().setUserId(entity.getCard().getUserId()); + response.getCard().setUserId(userOfTargetAccount.getLocalId()); response.getCard().setStackId(targetFullStack.getLocalId()); + entity.getCard().setUserId(userOfTargetAccount.getLocalId()); + entity.getCard().setStackId(targetFullStack.getLocalId()); }); try { @@ -1153,10 +1192,10 @@ public class SyncManager { originalLabel.setLocalId(null); originalLabel.setStatusEnum(DBStatus.LOCAL_EDITED); originalLabel.setAccountId(targetBoard.getAccountId()); - createAndAssignLabelToCard(originalBoard.getAccountId(), originalLabel, newCardId); + createAndAssignLabelToCard(targetBoard.getAccountId(), originalLabel, newCardId, serverToUse); } } else { - assignLabelToCard(existingMatch, targetCard); + assignLabelToCard(existingMatch, targetCard, serverToUse); } } @@ -1215,14 +1254,18 @@ public class SyncManager { return liveData; } - @AnyThread public MutableLiveData<Label> createAndAssignLabelToCard(long accountId, @NonNull Label label, long localCardId) { + return createAndAssignLabelToCard(accountId, label, localCardId, serverAdapter); + } + + @AnyThread + private MutableLiveData<Label> createAndAssignLabelToCard(long accountId, @NonNull Label label, long localCardId, ServerAdapter serverAdapterToUse) { MutableLiveData<Label> liveData = new MutableLiveData<>(); doAsync(() -> { Account account = dataBaseAdapter.getAccountByIdDirectly(accountId); Board board = dataBaseAdapter.getBoardByLocalCardIdDirectly(localCardId); label.setAccountId(accountId); - new DataPropagationHelper(serverAdapter, dataBaseAdapter).createEntity(new LabelDataProvider(null, board, null), label, new IResponseCallback<Label>(account) { + new DataPropagationHelper(serverAdapterToUse, dataBaseAdapter).createEntity(new LabelDataProvider(null, board, null), label, new IResponseCallback<Label>(account) { @Override public void onResponse(Label response) { assignLabelToCard(response, dataBaseAdapter.getCardByLocalIdDirectly(accountId, localCardId)); @@ -1292,6 +1335,11 @@ public class SyncManager { @AnyThread public void assignLabelToCard(@NonNull Label label, @NonNull Card card) { + assignLabelToCard(label, card, serverAdapter); + } + + @AnyThread + public void assignLabelToCard(@NonNull Label label, @NonNull Card card, ServerAdapter serverAdapterToUse) { doAsync(() -> { final long localLabelId = label.getLocalId(); final long localCardId = card.getLocalId(); @@ -1302,8 +1350,8 @@ public class SyncManager { Stack stack = dataBaseAdapter.getStackByLocalIdDirectly(card.getStackId()); Board board = dataBaseAdapter.getBoardByLocalIdDirectly(stack.getBoardId()); Account account = dataBaseAdapter.getAccountByIdDirectly(card.getAccountId()); - if (serverAdapter.hasInternetConnection()) { - serverAdapter.assignLabelToCard(board.getId(), stack.getId(), card.getId(), label.getId(), new IResponseCallback<Void>(account) { + if (serverAdapterToUse.hasInternetConnection()) { + serverAdapterToUse.assignLabelToCard(board.getId(), stack.getId(), card.getId(), label.getId(), new IResponseCallback<Void>(account) { @Override public void onResponse(Void response) { @@ -1639,15 +1687,14 @@ public class SyncManager { @AnyThread private static Attachment populateAttachmentEntityForFile(@NonNull Attachment target, long localCardId, @NonNull String mimeType, @NonNull File file) { - Attachment attachment = target; - attachment.setCardId(localCardId); - attachment.setMimetype(mimeType); - attachment.setData(file.getName()); - attachment.setFilename(file.getName()); - attachment.setBasename(file.getName()); - attachment.setLocalPath(file.getAbsolutePath()); - attachment.setFilesize(file.length()); - return attachment; + target.setCardId(localCardId); + target.setMimetype(mimeType); + target.setData(file.getName()); + target.setFilename(file.getName()); + target.setBasename(file.getName()); + target.setLocalPath(file.getAbsolutePath()); + target.setFilesize(file.length()); + return target; } @AnyThread diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/DataBaseAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/DataBaseAdapter.java index 507882ee7..30a6a6b49 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/DataBaseAdapter.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/DataBaseAdapter.java @@ -940,4 +940,8 @@ public class DataBaseAdapter { public List<Account> readAccountsForHostWithReadAccessToBoardDirectly(String host, long boardRemoteId) { return db.getAccountDao().readAccountsForHostWithReadAccessToBoardDirectly("%"+host+"%", boardRemoteId); } + + public Board getBoardForAccountByNameDirectly(long account, String title) { + return db.getBoardDao().getBoardForAccountByNameDirectly(account, title); + } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/BoardDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/BoardDao.java index bd189e846..5b61f926e 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/BoardDao.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/BoardDao.java @@ -52,7 +52,7 @@ public interface BoardDao extends GenericDao<Board> { @Query("SELECT b.* FROM board b JOIN stack s ON s.boardId = b.localId JOIN card c ON c.localId = :localCardId") Board getBoardByLocalCardIdDirectly(long localCardId); - @Query("SELECT b.* FROM board b JOIN stack s ON s.boardId = b.localId JOIN card c ON c.localId = :localCardId") + @Query("SELECT b.* FROM board b JOIN stack s ON s.boardId = b.localId JOIN card c ON c.localId = :localCardId and c.stackId = s.localId") FullBoard getFullBoardByLocalCardIdDirectly(long localCardId); @Transaction @@ -72,4 +72,7 @@ public interface BoardDao extends GenericDao<Board> { @Query("SELECT count(*) FROM board WHERE accountId = :accountId and archived = 1 and (deletedAt = 0 or deletedAt is null) and status <> 3") LiveData<Integer> countArchivedBoards(long accountId); + + @Query("SELECT * FROM board WHERE accountId = :accountId and title = :title") + Board getBoardForAccountByNameDirectly(long accountId, String title); }
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/util/extrawurst/UserSearchLiveData.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/util/extrawurst/UserSearchLiveData.java index 46e77c415..179d816eb 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/util/extrawurst/UserSearchLiveData.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/util/extrawurst/UserSearchLiveData.java @@ -34,8 +34,6 @@ public class UserSearchLiveData extends MediatorLiveData<List<User>> implements this.accountId = accountId; this.searchTerm = searchTerm; this.notYetAssignedInACL = notYetAssignedInACL; - // TODO: remove log when stable - DeckLog.info("###DeckUserSearch: UI triggered! term: " + searchTerm); new Thread(() -> debouncer.call(notYetAssignedInACL)).start(); return this; } @@ -72,8 +70,6 @@ public class UserSearchLiveData extends MediatorLiveData<List<User>> implements } } if (!term.equals(searchTerm)) { - // TODO: remove log when stable - DeckLog.info("###DeckUserSearch: skip posting for term " + term + ": current searchTerm is " + searchTerm); return; } postCurrentFromDB(term); @@ -93,7 +89,5 @@ public class UserSearchLiveData extends MediatorLiveData<List<User>> implements private void postCurrentFromDB(String term) { List<User> foundInDB = db.searchUserByUidOrDisplayNameForACLDirectly(accountId, notYetAssignedInACL, term); postValue(foundInDB); - // TODO: remove log when stable - DeckLog.info("###DeckUserSearch: posting for term " + term + ": " + foundInDB); } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/SyncHelper.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/SyncHelper.java index 18d8c3672..f53696a8d 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/SyncHelper.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/SyncHelper.java @@ -37,6 +37,11 @@ public class SyncHelper { if (response != null) { provider.goingDeeper(); for (T entityFromServer : response) { + if (entityFromServer == null) { + // see https://github.com/stefan-niedermann/nextcloud-deck/issues/574 + DeckLog.error("Skipped null value from server for DataProvider: " + provider.getClass().getSimpleName()); + continue; + } entityFromServer.setAccountId(accountId); T existingEntity = provider.getSingleFromDB(dataBaseAdapter, accountId, entityFromServer); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/LabelDataProvider.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/LabelDataProvider.java index 403d71f87..7510d9be3 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/LabelDataProvider.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/LabelDataProvider.java @@ -3,6 +3,7 @@ package it.niedermann.nextcloud.deck.persistence.sync.helpers.providers; import java.util.Date; import java.util.List; +import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.api.IResponseCallback; import it.niedermann.nextcloud.deck.exceptions.HandledServerErrors; import it.niedermann.nextcloud.deck.model.Board; @@ -63,9 +64,12 @@ public class LabelDataProvider extends AbstractSyncDataProvider<Label> { @Override public void onError(Throwable throwable) { if (HandledServerErrors.LABELS_TITLE_MUST_BE_UNIQUE == HandledServerErrors.fromThrowable(throwable)){ + DeckLog.log(throwable.getCause().getMessage() + ": " + entitiy.toString()); dataBaseAdapter.deleteLabelPhysically(entitiy); + responder.onResponse(entitiy); + } else { + responder.onError(throwable); } - responder.onError(throwable); } }; } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/partial/BoardWitAclDownSyncDataProvider.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/partial/BoardWithAclDownSyncDataProvider.java index 326d257ab..8516f0fc0 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/partial/BoardWitAclDownSyncDataProvider.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/partial/BoardWithAclDownSyncDataProvider.java @@ -11,7 +11,7 @@ import it.niedermann.nextcloud.deck.persistence.sync.helpers.SyncHelper; import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.AccessControlDataProvider; import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.BoardDataProvider; -public class BoardWitAclDownSyncDataProvider extends BoardDataProvider { +public class BoardWithAclDownSyncDataProvider extends BoardDataProvider { @Override public void goDeeper(SyncHelper syncHelper, FullBoard existingEntity, FullBoard entityFromServer, IResponseCallback<Boolean> callback) { diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/partial/BoardWithStacksAndLabelsUpSyncDataProvider.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/partial/BoardWithStacksAndLabelsUpSyncDataProvider.java new file mode 100644 index 000000000..278971a9d --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/partial/BoardWithStacksAndLabelsUpSyncDataProvider.java @@ -0,0 +1,37 @@ +package it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.partial; + +import java.util.Collections; +import java.util.Date; +import java.util.List; + +import it.niedermann.nextcloud.deck.api.IResponseCallback; +import it.niedermann.nextcloud.deck.model.full.FullBoard; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.ServerAdapter; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.DataBaseAdapter; +import it.niedermann.nextcloud.deck.persistence.sync.helpers.SyncHelper; +import it.niedermann.nextcloud.deck.persistence.sync.helpers.providers.BoardDataProvider; + +public class BoardWithStacksAndLabelsUpSyncDataProvider extends BoardDataProvider { + + private FullBoard board; + + public BoardWithStacksAndLabelsUpSyncDataProvider(FullBoard boardToSync) { + board = boardToSync; + } + + @Override + public List<FullBoard> getAllChangedFromDB(DataBaseAdapter dataBaseAdapter, long accountId, Date lastSync) { + return Collections.singletonList(board); + } + + @Override + public void goDeeper(SyncHelper syncHelper, FullBoard existingEntity, FullBoard entityFromServer, IResponseCallback<Boolean> callback) { + // do nothing! + + } + + @Override + public void handleDeletes(ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, long accountId, List<FullBoard> entitiesFromServer) { + // do nothing! + } +} 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..ce4c279a0 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 @@ -508,7 +508,12 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener @Override public void onUpdateBoard(FullBoard fullBoard) { - syncManager.updateBoard(fullBoard); + final WrappedLiveData<FullBoard> updateLiveData = syncManager.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) { @@ -946,7 +951,14 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener EditBoardDialogFragment.newInstance().show(getSupportFragmentManager(), addBoard); } } - syncManager.deleteBoard(board); + + final WrappedLiveData<Void> deleteLiveData = syncManager.deleteBoard(board); + observeOnce(deleteLiveData, this, (next) -> { + if (deleteLiveData.hasError()) { + ExceptionDialogFragment.newInstance(deleteLiveData.getError(), mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + } + }); + binding.drawerLayout.closeDrawer(GravityCompat.START); } @@ -968,4 +980,23 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener public void onArchive(@NonNull Board board) { syncManager.archiveBoard(board); } + + @Override + public void onClone(Board board) { + 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 = syncManager.cloneBoard(board.getAccountId(), board.getLocalId(), board.getAccountId(), board.getColor()); + 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(); + } + })); + } }
\ No newline at end of file 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..c159ff98e 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"; @@ -80,16 +84,31 @@ public class ArchivedBoardsActvitiy extends BrandedActivity implements DeleteBoa @Override public void onBoardDeleted(Board board) { - syncManager.deleteBoard(board); + final WrappedLiveData<Void> deleteLiveData = syncManager.deleteBoard(board); + observeOnce(deleteLiveData, this, (next) -> { + if (deleteLiveData.hasError()) { + ExceptionDialogFragment.newInstance(deleteLiveData.getError(), viewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + } + }); } @Override public void onUpdateBoard(FullBoard fullBoard) { - syncManager.updateBoard(fullBoard); + final WrappedLiveData<FullBoard> updateLiveData = syncManager.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); } + + @Override + public void onClone(Board board) { + + } } 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 bc2891360..a5a065cd1 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 @@ -12,8 +12,8 @@ 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.card.AbstractCardViewHolder; import it.niedermann.nextcloud.deck.ui.card.CardAdapter; -import it.niedermann.nextcloud.deck.ui.card.CardViewHolder; import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment; import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce; @@ -26,7 +26,7 @@ public class ArchivedCardsAdapter extends CardAdapter { } @Override - public void onBindViewHolder(@NonNull CardViewHolder viewHolder, int position) { + public void onBindViewHolder(@NonNull AbstractCardViewHolder viewHolder, int position) { viewHolder.bind(cardList.get(position), account, boardRemoteId, hasEditPermission, R.menu.archived_card_menu, this, counterMaxValue, mainColor); } 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/accesscontrol/AccessControlDialogFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/accesscontrol/AccessControlDialogFragment.java index 33c0fe5b1..9c1d63195 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 @@ -103,7 +103,12 @@ public class AccessControlDialogFragment extends BrandedDialogFragment implement @Override public void updateAccessControl(AccessControl accessControl) { - syncManager.updateAccessControl(accessControl); + WrappedLiveData<AccessControl> updateLiveData = syncManager.updateAccessControl(accessControl); + observeOnce(updateLiveData, requireActivity(), (next) -> { + if (updateLiveData.hasError()) { + ExceptionDialogFragment.newInstance(updateLiveData.getError(), viewModel.getCurrentAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + } + }); } @Override @@ -129,7 +134,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 = syncManager.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/branding/BrandedSnackbar.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedSnackbar.java index 20e6f8dc8..8f7219b11 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 @@ -27,7 +27,7 @@ public class BrandedSnackbar { @ColorInt final int color = readBrandMainColor(view.getContext()); snackbar.setActionTextColor(ColorUtil.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/card/AbstractCardViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/AbstractCardViewHolder.java new file mode 100644 index 000000000..984f7099f --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/AbstractCardViewHolder.java @@ -0,0 +1,121 @@ +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.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().getTime())); + ViewUtil.themeDueDate(context, cardDueDate, card.getDueDate()); + } + + @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 5910850fa..27a6518ee 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,6 +1,5 @@ package it.niedermann.nextcloud.deck.ui.card; -import android.annotation.SuppressLint; import android.content.ClipData; import android.content.Context; import android.content.Intent; @@ -9,42 +8,48 @@ import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; +import androidx.core.content.ContextCompat; import androidx.fragment.app.FragmentManager; import androidx.lifecycle.LifecycleOwner; import androidx.recyclerview.widget.RecyclerView; 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.Stack; 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.branding.Branded; -import it.niedermann.nextcloud.deck.ui.branding.BrandedAlertDialogBuilder; import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment; +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<CardViewHolder> implements DragAndDropAdapter<FullCard>, CardOptionsItemSelectedListener, Branded { +public class CardAdapter extends RecyclerView.Adapter<AbstractCardViewHolder> implements DragAndDropAdapter<FullCard>, CardOptionsItemSelectedListener, Branded { + private final boolean compactMode; + @NonNull protected final SyncManager syncManager; - + @NonNull protected final FragmentManager fragmentManager; + @NonNull protected final Account account; @Nullable protected final Long boardRemoteId; @@ -55,12 +60,13 @@ public class CardAdapter extends RecyclerView.Adapter<CardViewHolder> implements private final Context context; @Nullable private final SelectCardListener selectCardListener; - protected List<FullCard> cardList = new LinkedList<>(); + @NonNull + protected List<FullCard> cardList = new ArrayList<>(); + @NonNull protected LifecycleOwner lifecycleOwner; @NonNull - final private List<FullStack> availableStacks = new ArrayList<>(); protected String counterMaxValue; - + @ColorInt protected int mainColor; @StringRes private int shareLinkRes; @@ -78,11 +84,8 @@ public class CardAdapter extends RecyclerView.Adapter<CardViewHolder> implements this.hasEditPermission = hasEditPermission; this.syncManager = syncManager; this.selectCardListener = selectCardListener; - this.mainColor = context.getResources().getColor(R.color.defaultBrand); - syncManager.getStacksForBoard(account.getId(), boardLocalId).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); } @@ -93,13 +96,35 @@ public class CardAdapter extends RecyclerView.Adapter<CardViewHolder> implements @NonNull @Override - public CardViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int position) { - return new CardViewHolder(ItemCardBinding.inflate(LayoutInflater.from(viewGroup.getContext()), viewGroup, false)); + public AbstractCardViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) { + switch (viewType) { + case R.layout.item_card_compact: + return new CompactCardViewHolder(ItemCardCompactBinding.inflate(LayoutInflater.from(viewGroup.getContext()), viewGroup, false)); + case R.layout.item_card_default_only_title: + return new DefaultCardOnlyTitleViewHolder(ItemCardDefaultOnlyTitleBinding.inflate(LayoutInflater.from(viewGroup.getContext()), viewGroup, false)); + case R.layout.item_card_default: + default: + return new DefaultCardViewHolder(ItemCardDefaultBinding.inflate(LayoutInflater.from(viewGroup.getContext()), viewGroup, false)); + } + } + + @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.getCommentCount() == 0) { + return R.layout.item_card_default_only_title; + } + return R.layout.item_card_default; + } } - @SuppressLint("SetTextI18n") @Override - public void onBindViewHolder(@NonNull CardViewHolder viewHolder, int position) { + public void onBindViewHolder(@NonNull AbstractCardViewHolder viewHolder, int position) { @NonNull FullCard fullCard = cardList.get(position); viewHolder.bind(fullCard, account, boardRemoteId, hasEditPermission, R.menu.card_menu, this, counterMaxValue, mainColor); @@ -128,7 +153,7 @@ public class CardAdapter extends RecyclerView.Adapter<CardViewHolder> implements @Override public int getItemCount() { - return cardList == null ? 0 : cardList.size(); + return cardList.size(); } public void insertItem(FullCard fullCard, int position) { @@ -136,6 +161,7 @@ public class CardAdapter extends RecyclerView.Adapter<CardViewHolder> implements notifyItemInserted(position); } + @NonNull @Override public List<FullCard> getItemList() { return this.cardList; @@ -186,28 +212,8 @@ public class CardAdapter extends RecyclerView.Adapter<CardViewHolder> implements 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; - } - } - 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(); + 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(), boardLocalId, fullCard.getLocalId()).show(fragmentManager, MoveCardDialogFragment.class.getSimpleName()); return true; } case R.id.action_card_archive: { 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/CardViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/DefaultCardViewHolder.java index 279d38360..5e82061d1 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/CardViewHolder.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/DefaultCardViewHolder.java @@ -1,19 +1,16 @@ 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.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; @@ -22,21 +19,18 @@ import org.jetbrains.annotations.Contract; import java.util.List; import it.niedermann.nextcloud.deck.R; -import it.niedermann.nextcloud.deck.databinding.ItemCardBinding; +import it.niedermann.nextcloud.deck.databinding.ItemCardDefaultBinding; 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.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 class CardViewHolder extends RecyclerView.ViewHolder { - private ItemCardBinding binding; +public class DefaultCardViewHolder extends AbstractCardViewHolder { + private ItemCardDefaultBinding binding; @SuppressWarnings("WeakerAccess") - public CardViewHolder(@NonNull ItemCardBinding binding) { + public DefaultCardViewHolder(@NonNull ItemCardDefaultBinding binding) { super(binding.getRoot()); this.binding = binding; } @@ -45,12 +39,9 @@ public class CardViewHolder extends RecyclerView.ViewHolder { * 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) { - final Context context = itemView.getContext(); + super.bind(fullCard, account, boardRemoteId, hasEditPermission, optionsMenu, optionsItemsSelectedListener, counterMaxValue, mainColor); - bindCardClickListener(null); - bindCardLongClickListener(null); - binding.cardMenu.setVisibility(hasEditPermission ? View.VISIBLE : View.GONE); - binding.cardTitle.setText(fullCard.getCard().getTitle().trim()); + final Context context = itemView.getContext(); if (fullCard.getAssignedUsers() != null && fullCard.getAssignedUsers().size() > 0) { binding.overlappingAvatars.setAvatars(account, fullCard.getAssignedUsers()); @@ -59,16 +50,6 @@ public class CardViewHolder extends RecyclerView.ViewHolder { binding.overlappingAvatars.setVisibility(View.GONE); } - DrawableCompat.setTint(binding.notSyncedYet.getDrawable(), mainColor); - binding.notSyncedYet.setVisibility(DBStatus.LOCAL_EDITED.equals(fullCard.getStatusEnum()) ? View.VISIBLE : View.GONE); - - if (fullCard.getCard().getDueDate() != null) { - setupDueDate(binding.cardDueDate, fullCard.getCard()); - binding.cardDueDate.setVisibility(View.VISIBLE); - } else { - binding.cardDueDate.setVisibility(View.GONE); - } - final int attachmentsCount = fullCard.getAttachments().size(); if (attachmentsCount == 0) { @@ -104,23 +85,31 @@ public class CardViewHolder extends RecyclerView.ViewHolder { } else { binding.cardCountTasks.setVisibility(View.GONE); } + } - binding.cardMenu.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); - } + @Override + protected TextView getCardDueDate() { + return binding.cardDueDate; + } + + @Override + protected ImageView getNotSyncedYet() { + return binding.notSyncedYet; + } + + @Override + protected TextView getCardTitle() { + return binding.cardTitle; + } - popup.setOnMenuItemClickListener(item -> optionsItemsSelectedListener.onCardOptionsItemSelected(item, fullCard)); - popup.show(); - }); + @Override + protected View getCardMenu() { + return binding.cardMenu; + } + + @Override + protected MaterialCardView getCard() { + return binding.card; } public void bindCardClickListener(@Nullable OnClickListener l) { @@ -135,11 +124,6 @@ public class CardViewHolder extends RecyclerView.ViewHolder { return binding.card; } - private static void setupDueDate(@NonNull TextView cardDueDate, @NonNull Card card) { - final Context context = cardDueDate.getContext(); - cardDueDate.setText(DateUtil.getRelativeDateTimeString(context, card.getDueDate().getTime())); - ViewUtil.themeDueDate(context, cardDueDate, card.getDueDate()); - } private static void setupCounter(@NonNull TextView textView, @NonNull String counterMaxValue, int count) { if (count > 99) { 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/CardAttachmentsFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentsFragment.java index c291c5bdf..23f6377a5 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,7 +1,6 @@ package it.niedermann.nextcloud.deck.ui.card.attachments; import android.Manifest; -import android.app.Activity; import android.content.ContentResolver; import android.content.Intent; import android.net.Uri; @@ -16,6 +15,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.core.app.SharedElementCallback; +import androidx.core.content.PermissionChecker; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.GridLayoutManager; @@ -43,6 +43,8 @@ import it.niedermann.nextcloud.deck.ui.branding.BrandedSnackbar; import it.niedermann.nextcloud.deck.ui.card.EditCardViewModel; import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment; +import static android.app.Activity.RESULT_OK; +import static androidx.core.content.PermissionChecker.checkSelfPermission; 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.card.attachments.CardAttachmentAdapter.VIEW_TYPE_DEFAULT; @@ -55,8 +57,8 @@ public class CardAttachmentsFragment extends BrandedFragment implements Attachme private FragmentCardEditTabAttachmentsBinding binding; private EditCardViewModel viewModel; - private static final int REQUEST_CODE_ADD_ATTACHMENT = 1; - private static final int REQUEST_PERMISSION = 2; + private static final int REQUEST_CODE_ADD_FILE = 1; + private static final int REQUEST_CODE_ADD_FILE_PERMISSION = 2; private SyncManager syncManager; private CardAttachmentAdapter adapter; @@ -123,14 +125,11 @@ public class CardAttachmentsFragment extends BrandedFragment implements Attachme updateEmptyContentView(); } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && viewModel.canEdit()) { + if (viewModel.canEdit()) { binding.fab.setOnClickListener(v -> { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, - REQUEST_PERMISSION); - } else { - startFilePickerIntent(); - } + startActivityForResult(new Intent(Intent.ACTION_GET_CONTENT) + .addCategory(Intent.CATEGORY_OPENABLE) + .setType("*/*"), REQUEST_CODE_ADD_FILE); }); binding.fab.show(); binding.attachmentsList.addOnScrollListener(new RecyclerView.OnScrollListener() { @@ -150,110 +149,124 @@ public class CardAttachmentsFragment extends BrandedFragment implements Attachme } @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); + public void pickFile() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && checkSelfPermission(requireActivity(), Manifest.permission.READ_CONTACTS) != PermissionChecker.PERMISSION_GRANTED) { + requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, + REQUEST_CODE_ADD_FILE_PERMISSION); + } else { + Intent intent = new Intent(Intent.ACTION_GET_CONTENT) + .addCategory(Intent.CATEGORY_OPENABLE) + .setType("*/*"); + startActivityForResult(intent, REQUEST_CODE_ADD_FILE); + } } @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; - } - final Uri sourceUri = data.getData(); - if (sourceUri == null) { - ExceptionDialogFragment.newInstance(new UploadAttachmentFailedException("sourceUri is null"), viewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); - return; - } - if (!ContentResolver.SCHEME_CONTENT.equals(sourceUri.getScheme())) { - ExceptionDialogFragment.newInstance(new UploadAttachmentFailedException("Unknown URI scheme: " + sourceUri.getScheme()), viewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); - return; - } - - DeckLog.verbose("--- found content URL " + sourceUri.getPath()); - File fileToUpload; + switch (requestCode) { + case REQUEST_CODE_ADD_FILE: { + if (resultCode == RESULT_OK) { + if (data == null) { + ExceptionDialogFragment.newInstance(new UploadAttachmentFailedException("Intent data is null"), viewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + return; + } + final Uri sourceUri = data.getData(); + if (sourceUri == null) { + ExceptionDialogFragment.newInstance(new UploadAttachmentFailedException("sourceUri is null"), viewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + return; + } + if (!ContentResolver.SCHEME_CONTENT.equals(sourceUri.getScheme())) { + ExceptionDialogFragment.newInstance(new UploadAttachmentFailedException("Unknown URI scheme: " + sourceUri.getScheme()), viewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + return; + } - 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; - } + DeckLog.verbose("--- found content URL " + sourceUri.getPath()); + File fileToUpload; - 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; - } - } + 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; + } - 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); + 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(); - } else { - ExceptionDialogFragment.newInstance(new UploadAttachmentFailedException("Unknown URI scheme", t), viewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + return; } - } else { - viewModel.getFullCard().getAttachments().remove(a); - adapter.removeAttachment(a); - viewModel.getFullCard().getAttachments().add(next); - adapter.addAttachment(next); } - }); + + 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(); + } else { + ExceptionDialogFragment.newInstance(new UploadAttachmentFailedException("Unknown URI scheme", t), viewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + } + } else { + viewModel.getFullCard().getAttachments().remove(a); + adapter.removeAttachment(a); + viewModel.getFullCard().getAttachments().add(next); + adapter.addAttachment(next); + } + }); + } + updateEmptyContentView(); + } + break; + } + default: { + super.onActivityResult(requestCode, resultCode, data); } - 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(); - } - } else { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); + switch (requestCode) { + case REQUEST_CODE_ADD_FILE_PERMISSION: + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + pickFile(); + } + 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()); + final WrappedLiveData<Void> deleteLiveData = syncManager.deleteAttachmentOfCard(viewModel.getAccount().getId(), viewModel.getFullCard().getLocalId(), attachment.getLocalId()); + observeOnce(deleteLiveData, this, (next) -> { + if (deleteLiveData.hasError()) { + ExceptionDialogFragment.newInstance(deleteLiveData.getError(), viewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + } + }); } updateEmptyContentView(); } @@ -263,7 +276,6 @@ 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); @@ -278,4 +290,8 @@ public class CardAttachmentsFragment extends BrandedFragment implements Attachme public void applyBrand(int mainColor) { applyBrandToFAB(mainColor, binding.fab); } + + public static Fragment newInstance() { + return new CardAttachmentsFragment(); + } } 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..97a011398 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/movecard/MoveCardDialogFragment.java @@ -0,0 +1,124 @@ +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.model.full.FullStack; +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 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_LOCAL_ID = "card_local_id"; + private Long originAccountId; + private Long originBoardLocalId; + private Long originCardLocalId; + + private DialogMoveCardBinding binding; + private PickStackFragment fragment; + private MoveCardListener moveCardListener; + + private Account selectedAccount; + private Board selectedBoard; + private FullStack 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); + } + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + binding = DialogMoveCardBinding.inflate(inflater); + 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) { + fragment = new PickStackFragment(); + getChildFragmentManager() + .beginTransaction() + .add(R.id.fragment_container, fragment) + .commit(); + } + + @Override + public void onStackPicked(@NonNull Account account, @Nullable Board board, @Nullable FullStack fullStack) { + this.selectedAccount = account; + this.selectedBoard = board; + this.selectedStack = fullStack; + if (board == null || fullStack == 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, 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.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..6ebbed99b --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackFragment.java @@ -0,0 +1,195 @@ +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 java.util.List; + +import it.niedermann.nextcloud.deck.DeckLog; +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.full.FullStack; +import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; +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 SyncManager syncManager; + + private PickStackListener pickStackListener; + + 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); + + 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<FullStack>> stacksLiveData; + @NonNull + private Observer<List<FullStack>> stacksObserver = (fullStacks) -> { + stackAdapter.clear(); + stackAdapter.addAll(fullStacks); + + if (fullStacks.size() > 0) { + binding.stackSelect.setEnabled(true); + + FullStack fullStackToSelect = null; + for (FullStack fullStack : fullStacks) { + if (fullStack.getLocalId() == lastStackId) { + fullStackToSelect = fullStack; + break; + } + } + if (fullStackToSelect == null) { + fullStackToSelect = fullStacks.get(0); + } + binding.stackSelect.setSelection(stackAdapter.getPosition(fullStackToSelect)); + } 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()); + } + DeckLog.error("PICKSTACK: onAttach successful"); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + binding = FragmentPickStackBinding.inflate(getLayoutInflater()); + + 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); + + syncManager = new SyncManager(requireContext()); + + switchMap(syncManager.hasAccounts(), hasAccounts -> { + if (hasAccounts) { + return syncManager.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, syncManager.getBoardsWithEditPermission(parent.getSelectedItemId())); + }); + + binding.boardSelect.setOnItemSelectedListener((SelectedListener) (parent, view, position, id) -> { + updateLiveDataSource(stacksLiveData, stacksObserver, syncManager.getStacksForBoard(binding.accountSelect.getSelectedItemId(), parent.getSelectedItemId())); + }); + + binding.stackSelect.setOnItemSelectedListener((SelectedListener) (parent, view, position, id) -> { + pickStackListener.onStackPicked((Account) binding.accountSelect.getSelectedItem(), (Board) binding.boardSelect.getSelectedItem(), (FullStack) 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() { + return new PickStackFragment(); + } +}
\ 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..0abb4dac9 --- /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.full.FullStack; + +public interface PickStackListener { + void onStackPicked(@NonNull Account account, @Nullable Board board, @Nullable FullStack fullStack); +} 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/stack/StackFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/StackFragment.java index 726a74184..72c5ffc84 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,15 +20,22 @@ 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"; @@ -153,4 +160,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) { + WrappedLiveData<Void> liveData = syncManager.moveCard(originAccountId, originCardLocalId, targetAccountId, targetBoardLocalId, targetStackLocalId); + observeOnce(liveData, requireActivity(), (next) -> { + if (liveData.hasError()) { + 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/view/labelchip/CompactLabelChip.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labelchip/CompactLabelChip.java new file mode 100644 index 000000000..cf872f406 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labelchip/CompactLabelChip.java @@ -0,0 +1,22 @@ +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.R; +import it.niedermann.nextcloud.deck.model.Label; + +import static it.niedermann.nextcloud.deck.util.DimensionUtil.dpToPx; + +@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(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..ba3331457 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,4 +1,4 @@ -package it.niedermann.nextcloud.deck.ui.view; +package it.niedermann.nextcloud.deck.ui.view.labelchip; import android.annotation.SuppressLint; import android.content.Context; @@ -16,22 +16,20 @@ 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); @@ -43,9 +41,6 @@ public class LabelChip extends Chip { setTextEndPadding(gutter); setChipEndPadding(gutter); - setText(label.getTitle()); - setEllipsize(MIDDLE); - try { int labelColor = Color.parseColor("#" + label.getColor()); ColorStateList c = ColorStateList.valueOf(labelColor); 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..9a1d60021 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; @@ -14,14 +14,16 @@ import java.util.List; 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); @@ -82,9 +84,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/util/DrawerMenuUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/DrawerMenuUtil.java index 7457c0df7..3ee5bbe74 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/util/DrawerMenuUtil.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/util/DrawerMenuUtil.java @@ -63,6 +63,9 @@ public class DrawerMenuUtil { case R.id.manage_labels: ManageLabelsDialogFragment.newInstance(board.getLocalId()).show(context.getSupportFragmentManager(), editBoard); return true; + case R.id.clone_board: + context.onClone(board); + return true; case R.id.archive_board: context.onArchive(board); return true; diff --git a/app/src/main/res/drawable/ic_baseline_compact_24.xml b/app/src/main/res/drawable/ic_baseline_compact_24.xml new file mode 100644 index 000000000..d395a7338 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_compact_24.xml @@ -0,0 +1,5 @@ +<vector android:height="24dp" android:tint="#757575" + android:viewportHeight="24" android:viewportWidth="24" + android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> + <path android:fillColor="@android:color/white" android:pathData="M8,19h3v4h2v-4h3l-4,-4 -4,4zM16,5h-3L13,1h-2v4L8,5l4,4 4,-4zM4,11v2h16v-2L4,11z"/> +</vector> diff --git a/app/src/main/res/layout/activity_archived.xml b/app/src/main/res/layout/activity_archived.xml index 6dd7024dc..d5e9646a8 100644 --- a/app/src/main/res/layout/activity_archived.xml +++ b/app/src/main/res/layout/activity_archived.xml @@ -35,5 +35,5 @@ android:scrollbarStyle="outsideOverlay" android:scrollbars="vertical" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" - tools:listitem="@layout/item_card" /> + tools:listitem="@layout/item_card_default" /> </LinearLayout>
\ No newline at end of file diff --git a/app/src/main/res/layout/dialog_move_card.xml b/app/src/main/res/layout/dialog_move_card.xml new file mode 100644 index 000000000..e94f63b74 --- /dev/null +++ b/app/src/main/res/layout/dialog_move_card.xml @@ -0,0 +1,63 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:orientation="vertical" + android:padding="@dimen/spacer_1x"> + + <ScrollView + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + + <FrameLayout + android:id="@+id/fragment_container" + android:layout_width="wrap_content" + android:layout_height="wrap_content" /> + </ScrollView> + + <TextView + android:id="@+id/move_warning" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacer_2x" + android:drawableStart="@drawable/ic_warning_white_24dp" + android:drawablePadding="@dimen/spacer_3x" + android:paddingStart="@dimen/spacer_3x" + android:paddingEnd="@dimen/spacer_1x" + android:text="@string/move_warning" + android:textColor="@color/danger" + android:visibility="gone" + app:drawableTint="@color/danger" + tools:visibility="visible" /> + + <com.google.android.flexbox.FlexboxLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacer_1x" + android:orientation="horizontal" + android:paddingStart="@dimen/spacer_1x" + android:paddingEnd="@dimen/spacer_1x" + app:justifyContent="space_between"> + + <Button + android:id="@+id/cancel" + style="@style/Widget.MaterialComponents.Button.TextButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="start" + android:layout_marginEnd="@dimen/spacer_1x" + android:text="@android:string/cancel" + android:textColor="@color/defaultBrand" /> + + <Button + android:id="@+id/submit" + style="@style/Widget.MaterialComponents.Button.TextButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="end" + android:text="@string/action_card_move" + android:textColor="@color/defaultBrand" /> + </com.google.android.flexbox.FlexboxLayout> +</LinearLayout>
\ No newline at end of file diff --git a/app/src/main/res/layout/fragment_pick_stack.xml b/app/src/main/res/layout/fragment_pick_stack.xml new file mode 100644 index 000000000..9769969ff --- /dev/null +++ b/app/src/main/res/layout/fragment_pick_stack.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <androidx.appcompat.widget.AppCompatSpinner + android:id="@+id/account_select" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:prompt="@string/choose_account" + tools:listitem="@layout/item_prepare_create_account" /> + + <androidx.appcompat.widget.AppCompatSpinner + android:id="@+id/board_select" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:prompt="@string/choose_board" + tools:listitem="@layout/item_board" /> + + <androidx.appcompat.widget.AppCompatSpinner + android:id="@+id/stack_select" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:prompt="@string/choose_list" + tools:listitem="@layout/item_board" /> +</LinearLayout>
\ No newline at end of file diff --git a/app/src/main/res/layout/fragment_stack.xml b/app/src/main/res/layout/fragment_stack.xml index d4cbd2ee8..e0fb35c8a 100644 --- a/app/src/main/res/layout/fragment_stack.xml +++ b/app/src/main/res/layout/fragment_stack.xml @@ -24,5 +24,5 @@ android:scrollbarStyle="outsideOverlay" android:scrollbars="vertical" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" - tools:listitem="@layout/item_card" /> + tools:listitem="@layout/item_card_default" /> </LinearLayout>
\ No newline at end of file diff --git a/app/src/main/res/layout/item_card_compact.xml b/app/src/main/res/layout/item_card_compact.xml new file mode 100644 index 000000000..457aa0d25 --- /dev/null +++ b/app/src/main/res/layout/item_card_compact.xml @@ -0,0 +1,94 @@ +<?xml version="1.0" encoding="utf-8"?> +<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/card" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/spacer_2x" + android:layout_marginTop="@dimen/spacer_1x" + android:layout_marginEnd="@dimen/spacer_2x" + android:layout_marginBottom="@dimen/spacer_1x" + android:focusable="true" + app:cardBackgroundColor="@color/bg_card"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:paddingTop="@dimen/spacer_1x" + android:paddingBottom="@dimen/spacer_1x"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:paddingStart="@dimen/spacer_2x" + android:paddingEnd="@dimen/spacer_1x"> + + <TextView + android:id="@+id/card_title" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="4sp" + android:layout_weight="1" + android:textColor="?attr/colorAccent" + android:textSize="18sp" + tools:ignore="RtlSymmetry" + tools:text="Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut l" /> + + <ImageView + android:id="@+id/not_synced_yet" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="8sp" + android:contentDescription="@string/not_synced_yet" + android:visibility="gone" + app:srcCompat="@drawable/ic_sync_blue_24dp" + tools:visibility="visible" /> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:paddingStart="@dimen/spacer_1hx" + tools:ignore="RtlSymmetry"> + + <androidx.appcompat.widget.AppCompatTextView + android:id="@+id/card_due_date" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:background="@drawable/due_tomorrow_background" + android:drawablePadding="@dimen/spacer_1hx" + android:gravity="center" + android:padding="@dimen/spacer_1hx" + android:textColor="@color/fg_secondary" + app:drawableStartCompat="@drawable/calendar_blank_grey600_24dp" + tools:text="tomorrow" /> + + </LinearLayout> + + <ImageView + android:id="@+id/card_menu" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:background="?attr/selectableItemBackgroundBorderless" + android:contentDescription="@string/label_menu" + android:padding="@dimen/spacer_1hx" + android:tint="?attr/colorAccent" + app:srcCompat="@drawable/ic_menu" /> + </LinearLayout> + + <it.niedermann.nextcloud.deck.ui.view.labellayout.CompactLabelLayout + android:id="@+id/labels" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacer_1x" + android:animateLayoutChanges="true" + android:paddingStart="@dimen/spacer_2x" + android:paddingEnd="@dimen/spacer_2x" + app:flexWrap="nowrap" + tools:layout_height="@dimen/avatar_size" /> + + </LinearLayout> +</com.google.android.material.card.MaterialCardView>
\ No newline at end of file diff --git a/app/src/main/res/layout/item_card.xml b/app/src/main/res/layout/item_card_default.xml index c0457f5e4..01970ef57 100644 --- a/app/src/main/res/layout/item_card.xml +++ b/app/src/main/res/layout/item_card_default.xml @@ -69,7 +69,7 @@ </LinearLayout> </LinearLayout> - <it.niedermann.nextcloud.deck.ui.view.LabelLayout + <it.niedermann.nextcloud.deck.ui.view.labellayout.DefaultLabelLayout android:id="@+id/labels" android:layout_width="match_parent" android:layout_height="wrap_content" diff --git a/app/src/main/res/layout/item_card_default_only_title.xml b/app/src/main/res/layout/item_card_default_only_title.xml new file mode 100644 index 000000000..7d32e7f44 --- /dev/null +++ b/app/src/main/res/layout/item_card_default_only_title.xml @@ -0,0 +1,77 @@ +<?xml version="1.0" encoding="utf-8"?> +<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/card" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/spacer_2x" + android:layout_marginTop="@dimen/spacer_1x" + android:layout_marginEnd="@dimen/spacer_2x" + android:layout_marginBottom="@dimen/spacer_1x" + android:focusable="true" + app:cardBackgroundColor="@color/bg_card"> + + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:paddingStart="@dimen/spacer_2x" + android:paddingTop="@dimen/spacer_1x" + android:paddingEnd="@dimen/spacer_1x" + android:paddingBottom="@dimen/spacer_1x"> + + <TextView + android:id="@+id/card_title" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="4sp" + android:layout_weight="1" + android:textColor="?attr/colorAccent" + android:textSize="18sp" + tools:ignore="RtlSymmetry" + tools:text="Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut l" /> + + <ImageView + android:id="@+id/not_synced_yet" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="8sp" + android:contentDescription="@string/not_synced_yet" + android:visibility="gone" + app:srcCompat="@drawable/ic_sync_blue_24dp" + tools:visibility="visible" /> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:paddingStart="@dimen/spacer_1hx" + tools:ignore="RtlSymmetry"> + + <androidx.appcompat.widget.AppCompatTextView + android:id="@+id/card_due_date" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:background="@drawable/due_tomorrow_background" + android:drawablePadding="@dimen/spacer_1hx" + android:gravity="center" + android:padding="@dimen/spacer_1hx" + android:textColor="@color/fg_secondary" + app:drawableStartCompat="@drawable/calendar_blank_grey600_24dp" + tools:text="tomorrow" /> + + </LinearLayout> + + <ImageView + android:id="@+id/card_menu" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:background="?attr/selectableItemBackgroundBorderless" + android:contentDescription="@string/label_menu" + android:padding="@dimen/spacer_1hx" + android:tint="?attr/colorAccent" + app:srcCompat="@drawable/ic_menu" /> + </LinearLayout> +</com.google.android.material.card.MaterialCardView>
\ No newline at end of file diff --git a/app/src/main/res/menu/navigation_context_menu.xml b/app/src/main/res/menu/navigation_context_menu.xml index d56f07595..ece2b917e 100644 --- a/app/src/main/res/menu/navigation_context_menu.xml +++ b/app/src/main/res/menu/navigation_context_menu.xml @@ -12,6 +12,11 @@ android:title="@string/manage_tags" app:showAsAction="never" /> <item + android:id="@+id/clone_board" + android:orderInCategory="10" + android:title="@string/clone_board" + app:showAsAction="never" /> + <item android:id="@+id/archive_board" android:orderInCategory="20" android:title="@string/archive_board" diff --git a/app/src/main/res/values-cs-rCZ/strings.xml b/app/src/main/res/values-cs-rCZ/strings.xml index 3bc6b422e..06d1fb151 100644 --- a/app/src/main/res/values-cs-rCZ/strings.xml +++ b/app/src/main/res/values-cs-rCZ/strings.xml @@ -133,6 +133,7 @@ <string name="delete_board_message">Toto tabuli nadobro smaže, včetně všech seznamů a karet.</string> <string name="settings_theme_title">Tmavý motiv vzhledu</string> <string name="settings_branding_title">Opatření vlastním logem</string> + <string name="settings_compact_title">Kompaktní režim 🆕</string> <string name="settings_background_sync">Synchronizace na pozadí</string> <string name="pref_value_wifi_and_mobile">Synchr. přes Wi-Fi a mobilní data</string> <string name="pref_value_wifi_only">Synchr. pouze přes Wi-Fi</string> diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 69f7a1886..b0e4128b8 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -129,6 +129,7 @@ <string name="delete_board_message">Dies wird dieses Board endgültig inklusive aller Listen und Karten löschen.</string> <string name="settings_theme_title">Dunkles Design</string> <string name="settings_branding_title">Branding</string> + <string name="settings_compact_title">Kompakt-Modus 🆕</string> <string name="settings_background_sync">Hintergrundsynchronisierung</string> <string name="pref_value_wifi_and_mobile">Über WLAN und mobile Daten synchronisieren</string> <string name="pref_value_wifi_only">Nur über WLAN synchronisieren</string> diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 5ef209258..cb1f66037 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -129,6 +129,7 @@ <string name="delete_board_message">Μόνιμη διαγραφή πίνακα με όλες τις λίστες και κάρτες.</string> <string name="settings_theme_title">Σκούρο θέμα</string> <string name="settings_branding_title">Επωνυμία</string> + <string name="settings_compact_title">Συμπαγής προβολή 🆕</string> <string name="settings_background_sync">Συγχρονισμός στο παρασκήνιο</string> <string name="pref_value_wifi_and_mobile">Συγχρονισμός σε Wi-Fi και δεδομένα κινητού</string> <string name="pref_value_wifi_only">Συγχρονισμός μόνο σε Wi-Fi</string> @@ -217,6 +218,7 @@ <string name="error_dialog_title">Ωχ όχι - Τώρα τί; 🙁</string> <string name="error_dialog_tip_token_mismatch_retry">Δοκιμάστε τερματισμό της εφαρμογή και επανεκκίνηση. Ίσως υπάρχει λάθος σύνδεση.</string> <string name="error_dialog_tip_token_mismatch_clear_storage">Εάν το πρόβλημα παραμένει, προσπαθήστε να εκκαθαρίσετε τον χώρο αποθήκευσης και των δύο εφαρμογών: του Nextcloud και του Nextcloud Deck για επίλυση.</string> + <string name="error_dialog_tip_database_upgrade_failed">Η αναβάθμιση της βάσης δεδομένων απέτυχε. Παρακαλούμε αναφέρετε το πρόβλημα και εκκαθαρίστε τον χώρο αποθήκευσης για να χρησιμοποιήσετε την εφαρμογή κανονικά.</string> <string name="error_dialog_tip_clear_storage">Μπορείτε να εκκαθαρίσετε τον χώρο αποθήκευσης από τις πληροφορίες εφαρμογής και επιλέγοντας Αποθηκευτικός χώρος → Εκκαθάριση χώρου αποθήκευσης.</string> <string name="error_dialog_tip_files_outdated">Η εφαρμογή Nextcloud δεν είναι ενημερωμένη. Παρακαλούμε επισκεφθείτε το Play Store ή το F-Groid για λήψη της τελευταίας έκδοσης.</string> <string name="error_dialog_tip_files_force_stop">Κάτι φαίνεται να πάει στραβά με την εφαρμογή Nextcloud. Προσπαθήστε να τερματίσετε την εφαρμογή Nextcloud και την εφαρμογή Nextcloud Deck.</string> @@ -232,6 +234,7 @@ <string name="error_dialog_version_not_parsable">Δεν μπορέσαμε να προσδιορίσουμε την έκδοση της εφαρμογής Deck του διακομιστή σας. Βεβαιωθείτε ότι είναι εγκατεστημένο και ενεργοποιημένο.</string> <string name="error_dialog_capabilities_not_parsable">Δεν ήταν δυνατός ο έλεγχος των δυνατοτήτων του διακομιστή σας. Βεβαιωθείτε ότι ο διακομιστής σας λειτουργεί σωστά και οι εφαρμογές έχουν πρόσβαση στο Nextcloud.</string> <string name="error_dialog_attachment_upload_failed">Δεν ήταν δυνατή η μεταφόρτωση ενός συνημμένου. Προσπαθήστε να το διαμοιράσετε με άλλο τρόπο και ενημερώστε μας σχετικά με το σφάλμα.</string> + <string name="error_dialog_tip_disable_battery_optimizations">Παρακαλούμε απενεργοποιήστε όλες τις βελτιστοποιήσεις μπαταρίας για το Nextcloud και την εφαρμογή Deck.</string> <string name="error_action_open_deck_info">Πληροφορίες εφαρμογής</string> <string name="error_action_open_network">Ρυθμίσεις δικτύου</string> <string name="error_action_server_logs">Αρχεία καταγραφής διακομιστή</string> @@ -253,4 +256,5 @@ <item quantity="other">%1$d σφάλματα κατά την μεταφόρτωση</item> </plurals> <string name="simple_report">Αναφορά</string> - </resources> + <string name="error_action_open_battery_settings">Ρυθμίσεις μπαταρίας</string> +</resources> diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index cf92736ed..9bd01d6dd 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -217,7 +217,6 @@ <string name="error_dialog_title">Oh no - ¿Ahora qué? 🙁</string> <string name="error_dialog_tip_token_mismatch_retry">Por favor, intenta forzar el cierre de la aplicación y vuelve a abrirla. Es posible que haya habido un problema de conexión con la aplicación Nextcloud.</string> <string name="error_dialog_tip_token_mismatch_clear_storage">Si el problema persiste, intenta limpiar borrar el almacenamiento de ambas apps, Nextcloud y Nextcloud Deck para solucionar este problema.</string> - <string name="error_dialog_tip_database_upgrade_failed">La actualización de la base de datos falló. Por favor, informe del problema y limpie el almacenamiento para usar la aplicación de manera normal.</string> <string name="error_dialog_tip_clear_storage">Puedes limpiar el almacenamiento abriendo la información de la app y seleccionando Almacenamiento → Eliminar datos.</string> <string name="error_dialog_tip_files_outdated">Su aplicación Nextcloud parece que está desactualizada. Por favor visite la Play Store o F-Droid para conseguir la versión más reciente.</string> <string name="error_dialog_tip_files_force_stop">Algo parece ir mal en tu app de Nextcloud. Por favor, intenta forzar el cierre de ambas, Nextcloud y Nextcloud Deck.</string> diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index 2f6ccb012..e704e9fcd 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -148,7 +148,7 @@ <string name="hours_6">6 ordu</string> <string name="action_card_move">Mugitu txartela</string> <string name="action_card_move_title">Mugitu %1$s</string> - <string name="please_add_an_account_first">Mesedez, gehitu kontu bat lehenengo</string> + <string name="please_add_an_account_first">Gehitu kontu bat lehenengo</string> <string name="title_is_mandatory">Izenburua jartzea nahitaezkoa da</string> <string name="provide_at_least_a_title_or_description">Jarri gutxienez izenburu edo deskribapen bat</string> <string name="welcome_text">Ongi etorri %1$s(e)ra</string> diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 89fa234ee..6c6d1792e 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -20,7 +20,7 @@ <string name="simple_error">Erreur</string> <string name="simple_exception">Exception</string> <string name="simple_close">fermer</string> - <string name="simple_open">Ouvert</string> + <string name="simple_open">Ouvrir</string> <string name="simple_switch">Permuter</string> <string name="simple_filter">Filtre</string> <string name="simple_overdue">En retard</string> @@ -129,6 +129,7 @@ <string name="delete_board_message">Cette action supprimera définitivement ce tableau incluant ses listes et ses cartes.</string> <string name="settings_theme_title">Thème sombre</string> <string name="settings_branding_title">Marque</string> + <string name="settings_compact_title">Mode compact</string> <string name="settings_background_sync">Synchronisation en tâche de fond</string> <string name="pref_value_wifi_and_mobile">Synchroniser en Wifi et données mobiles</string> <string name="pref_value_wifi_only">Synchroniser uniquement en Wi-Fi</string> @@ -217,6 +218,7 @@ <string name="error_dialog_title">Oh non ! - Et maintenant ? 🙁</string> <string name="error_dialog_tip_token_mismatch_retry">Essayez de forcer la fermeture de l\'application puis redémarrer la. Il y avait peut-être une mauvaise connexion à Nextcloud.</string> <string name="error_dialog_tip_token_mismatch_clear_storage">Si le problème persiste, essayez d\'effacer les données d\'application des deux applications: Nextcloud et Nextcloud Deck pour résoudre ce problème.</string> + <string name="error_dialog_tip_database_upgrade_failed">La mise à jour de la base de données a échoué. Veuillez envoyer l\'erreur et supprimer les données pour utiliser l\'application</string> <string name="error_dialog_tip_clear_storage">Vous pouvez nettoyer l\'espace de stockage en ouvrant les paramètres de l\'application et en sélectionnant Stockage → Effacer le stockage.</string> <string name="error_dialog_tip_files_outdated">Votre application Nextcloud semble ancienne. Veuillez visiter le Play Store ou F-Droid pour installer la dernière version.</string> <string name="error_dialog_tip_files_force_stop">Quelque chose semble ne pas fonctionner avec votre application Nextcloud. Essayer de forcer l\'arrêt des applications Nextcloud et Nextcloud Notes.</string> diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 6091b2f0b..e4a279121 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -114,8 +114,8 @@ <string name="do_you_want_to_save_your_changes">Confirma que quere gardar os cambios?</string> <string name="do_you_want_to_archive_all_cards_of_the_list">Quere arquivar todas as tarxetas de %1$s?</string> <plurals name="do_you_want_to_delete_the_current_list"> - <item quantity="one">Isto eliminará permanentemente %1$d tarxeta desta lista.</item> - <item quantity="other">Isto eliminará permanentemente as %1$d tarxetas desta lista.</item> + <item quantity="one">Isto eliminará de xeito permanente %1$d tarxeta desta lista.</item> + <item quantity="other">Isto eliminará de xeito permanente as %1$d tarxetas desta lista.</item> </plurals> <plurals name="do_you_want_to_delete_the_label"> <item quantity="one">Isto retirará a etiqueta de %1$d tarxeta.</item> @@ -129,6 +129,7 @@ <string name="delete_board_message">Isto eliminará este taboleiro de xeito permanente incluíndo todas as listas e tarxetas.</string> <string name="settings_theme_title">Tema escuro</string> <string name="settings_branding_title">Xestión da marca</string> + <string name="settings_compact_title">Modo compacto 🆕</string> <string name="settings_background_sync">Sincronización do traballo en segundo plano</string> <string name="pref_value_wifi_and_mobile">Sincronización con wifi e con datos móbiles</string> <string name="pref_value_wifi_only">Sincronizar só con wifi</string> diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index d9bde9148..b3cee302e 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -129,6 +129,7 @@ <string name="delete_board_message">Questo eliminerà definitivamente questa lavagna inclusi tutti gli elenchi e le schede.</string> <string name="settings_theme_title">Tema scuro</string> <string name="settings_branding_title">Marchio</string> + <string name="settings_compact_title">Modalità compatta 🆕</string> <string name="settings_background_sync">Sincronizzazione in background</string> <string name="pref_value_wifi_and_mobile">Sincronizza su Wi-FI e dati mobili</string> <string name="pref_value_wifi_only">Sincronizza solo con Wi-Fi</string> diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 4fd76266f..1dc85ba54 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -133,6 +133,7 @@ <string name="delete_board_message">Spowoduje to trwałe usunięcie tej tablicy, w tym wszystkich list i kart.</string> <string name="settings_theme_title">Ciemny motyw</string> <string name="settings_branding_title">Motyw serwera</string> + <string name="settings_compact_title">Tryb kompaktowy 🆕</string> <string name="settings_background_sync">Synchronizacja w tle</string> <string name="pref_value_wifi_and_mobile">Synchronizacja w sieci Wi-Fi i komórkowej transmisji danych</string> <string name="pref_value_wifi_only">Synchronizuj tylko przez Wi-Fi</string> @@ -221,6 +222,7 @@ <string name="error_dialog_title">O nie - co teraz? 🙁</string> <string name="error_dialog_tip_token_mismatch_retry">Spróbuj wymusić zamknięcie aplikacji i uruchomić ją ponownie. Być może połączenie z aplikacją Nextcloud było nieprawidłowe.</string> <string name="error_dialog_tip_token_mismatch_clear_storage">Jeśli problem będzie się powtarzać, spróbuj wyczyścić pamięć obu aplikacji: Nextcloud i Nextcloud Deck, aby rozwiązać ten problem.</string> + <string name="error_dialog_tip_database_upgrade_failed">Aktualizacja bazy danych nie powiodła się. Zgłoś problem oraz wyczyść pamięć, aby ponownie korzystać z aplikacji.</string> <string name="error_dialog_tip_clear_storage">Możesz wyczyścić pamięć, otwierając informacje o aplikacji i wybierając Pamięć → Wyczyść pamięć podręczną.</string> <string name="error_dialog_tip_files_outdated">Twoja aplikacja Nextcloud wydaje się, że jest nieaktualna. Odwiedź Sklep Play lub F-Droid, aby pobrać najnowszą wersję.</string> <string name="error_dialog_tip_files_force_stop">Wydaje się, że coś jest nie tak z Twoją aplikacją Nextcloud. Spróbuj wymusić zatrzymanie aplikacji Nextcloud i Nextcloud Deck.</string> diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 8eb21ff3b..8d8822f9e 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -129,6 +129,7 @@ <string name="delete_board_message">Isso excluirá permanentemente este painel incluindo listas e cartões.</string> <string name="settings_theme_title">Tema escuro</string> <string name="settings_branding_title">Marcação</string> + <string name="settings_compact_title">Modo compacto</string> <string name="settings_background_sync">Sincronização em segundo plano</string> <string name="pref_value_wifi_and_mobile">Sincronizar com Wi-Fi e dados móveis</string> <string name="pref_value_wifi_only">Sincronizar apenas com Wi-Fi</string> diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index bb95d8196..a338d3505 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -221,7 +221,6 @@ <string name="error_dialog_title">О нет, что теперь? 🙁</string> <string name="error_dialog_tip_token_mismatch_retry">Попробуйте принудительно закрыть приложение и запустить его снова. Это могло быть связано с некорректным подключением к приложению Nextcloud.</string> <string name="error_dialog_tip_token_mismatch_clear_storage">Если проблема повторяется, попробуйте для её решения очистить хранилище приложений Nextcloud и Nextcloud Карточки.</string> - <string name="error_dialog_tip_database_upgrade_failed">Сбой обновления базы данных. Пожалуйста сообщите о проблеме и очистите хранилище, чтобы корректно использовать приложение.</string> <string name="error_dialog_tip_clear_storage">Для очистки хранилища на откройте приложение «Настройки» и выберите Приложения → Nextcloud / Nextcloud Deck → Хранилище → Очистить хранилище</string> <string name="error_dialog_tip_files_outdated">Ваше приложение Nextcloud устарело. Установите новую версию с Play Store или F-Droid.</string> <string name="error_dialog_tip_files_force_stop">Похоже, что с приложением Nextcloud ведёт себя неожиданным образом. Попробуйте принудительно остановить приложения Nextcloud и Nextcloud Карточки. </string> diff --git a/app/src/main/res/values-sk-rSK/strings.xml b/app/src/main/res/values-sk-rSK/strings.xml index 0898dfb00..51b963d38 100644 --- a/app/src/main/res/values-sk-rSK/strings.xml +++ b/app/src/main/res/values-sk-rSK/strings.xml @@ -221,7 +221,6 @@ <string name="error_dialog_title">Ale nie - Čo teraz? 🙁</string> <string name="error_dialog_tip_token_mismatch_retry">Skúste vyinútiť zatvorenie aplikácie a znova ju reštartovať. Možno došlo k chybnému pripojeniu k aplikácii Nextcloud.</string> <string name="error_dialog_tip_token_mismatch_clear_storage">Ak problém pretrváva, pokúste sa tento problém vyriešiť vymazaním úložiska oboch aplikácií: Nextcloud a Nextcloud Deck.</string> - <string name="error_dialog_tip_database_upgrade_failed">Aktualizácia databázy zlyhala. Nahláste problém a vymažte ukladací priestor, aby ste aplikáciu mohli normálne používať.</string> <string name="error_dialog_tip_clear_storage">Úložisko môžete vyčistiť otvorením informácii o aplikácii a výberom Úložisko → Vyčistiť úložisko.</string> <string name="error_dialog_tip_files_outdated">Váš Nextcloud vyzerá byť zastaralý. Navštívte Play Store alebo F-Droid pre získanie najnovšej verzie.</string> <string name="error_dialog_tip_files_force_stop">Zdá sa, že s Nextcloudom niečo nie je v poriadku. Pokúste sa vynútiť zastavenie aplikácie Nextcloud aj aplikácie Nextcloud Deck.</string> diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index ad6075d5b..0af202087 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -112,6 +112,7 @@ <string name="not_synced_yet">Још није синхронизовано</string> <string name="no_lists_yet">Нема још спискова</string> <string name="do_you_want_to_save_your_changes">Да ли желите да сачувате измене?</string> + <string name="do_you_want_to_archive_all_cards_of_the_list">Да ли желите да архивирате све картице са %1$s?</string> <plurals name="do_you_want_to_delete_the_current_list"> <item quantity="one">Овим ћете заувек обрисати %1$d картицу са овог списка.</item> <item quantity="few">Овим ћете заувек обрисати %1$d картице са овог списка.</item> @@ -240,7 +241,19 @@ <string name="info_box_maintenance_mode">Сервер у режиму одржавања</string> <string name="info_box_version_not_supported">Серверска верзија %1$s није подржана, ажурирајте на %2$s</string> <string name="share_link">Веза дељења</string> + <string name="archive_cards">Архивирај картице</string> <string name="manage_accounts">Управљање налозима</string> + <string name="manage_list">Управљај списком</string> <string name="simple_reply">Одговори</string> + <string name="error_while_uploading_attachment">Грешка приликом отпремања прилога: %1$s</string> + <string name="append_text_to_description">Придодај на опис</string> + <string name="add_text_as_comment">Додај као коментар</string> + <string name="progress_count">%1$d од %2$d</string> + <plurals name="progress_error_count"> + <item quantity="one">%1$d грешка приликом отпремања.</item> + <item quantity="few">%1$d грешке приликом отпремања.</item> + <item quantity="other">%1$d грешака приликом отпремања.</item> + </plurals> <string name="simple_report">Пријави</string> - </resources> + <string name="error_action_open_battery_settings">Подешавања батерије</string> +</resources> diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index a34bb2f8a..7b35c3683 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -129,6 +129,7 @@ <string name="delete_board_message">Bu işlem içindeki tüm liste ve kartlarla birlikte bu panoyu silecek.</string> <string name="settings_theme_title">Koyu tema</string> <string name="settings_branding_title">Markalama</string> + <string name="settings_compact_title">Sıkışık kip 🆕</string> <string name="settings_background_sync">Arka planda eşitleme</string> <string name="pref_value_wifi_and_mobile">Wi-Fi ve mobil veri ile eşitlensin</string> <string name="pref_value_wifi_only">Yalnız Wi-Fi ile eşitlensin</string> diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 5ecd246df..54bff5fee 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -4,6 +4,8 @@ <dimen name="spacer_2x">16dp</dimen> <dimen name="spacer_3x">24dp</dimen> + <dimen name="compact_label_height">6dp</dimen> + <!-- Drawer header --> <dimen name="drawer_header_height">100dp</dimen> <dimen name="drawer_header_logo_size">42dp</dimen> diff --git a/app/src/main/res/values/setup.xml b/app/src/main/res/values/setup.xml index 247bb0697..c790a5868 100644 --- a/app/src/main/res/values/setup.xml +++ b/app/src/main/res/values/setup.xml @@ -8,6 +8,7 @@ <string name="pref_key_wifi_only" translatable="false">wifiOnly</string> <string name="pref_key_dark_theme" translatable="false">darkTheme</string> <string name="pref_key_branding" translatable="false">branding</string> + <string name="pref_key_compact" translatable="false">compact</string> <string name="pref_key_background_sync" translatable="false">backgroundSync</string> <string name="pref_value_background_sync_off">off</string> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8e4d9ec60..678921cf2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -154,6 +154,7 @@ <string name="delete_board_message">This will permanently delete this board including all lists and cards.</string> <string name="settings_theme_title">Dark theme</string> <string name="settings_branding_title">Branding</string> + <string name="settings_compact_title">Compact mode 🆕</string> <string name="settings_background_sync">Background synchronization</string> <string name="pref_value_wifi_and_mobile">Sync on Wi-Fi and mobile data</string> <string name="pref_value_wifi_only">Sync only on Wi-Fi</string> @@ -282,6 +283,10 @@ </plurals> <string name="simple_report">Report</string> <string name="error_action_open_battery_settings">Battery settings</string> + <string name="move_warning">Neither comments nor attachments can be transferred when moving the card to another board.</string> + <string name="clone_board">Clone board</string> + <string name="cloning_board">Cloning %1$s…</string> + <string name="successfully_cloned_board">Successfully cloned %1$s</string> <string name="widget_stack_title">Stack</string> <string name="widget_stack_header_icon">Widget header icon</string> diff --git a/app/src/main/res/xml/settings.xml b/app/src/main/res/xml/settings.xml index 14943fff6..6d67622bd 100644 --- a/app/src/main/res/xml/settings.xml +++ b/app/src/main/res/xml/settings.xml @@ -29,6 +29,12 @@ android:icon="@drawable/ic_format_paint_grey600_24dp" android:key="@string/pref_key_branding" android:title="@string/settings_branding_title" - app:defaultValue="true" /> + app:defaultValue="false" /> + + <it.niedermann.nextcloud.deck.ui.branding.BrandedSwitchPreference + android:icon="@drawable/ic_baseline_compact_24" + android:key="@string/pref_key_compact" + android:title="@string/settings_compact_title" + app:defaultValue="false" /> </it.niedermann.nextcloud.deck.ui.branding.BrandedPreferenceCategory> </PreferenceScreen> diff --git a/cross-tab-drag-and-drop/src/main/java/it/niedermann/android/crosstabdnd/DragAndDropAdapter.java b/cross-tab-drag-and-drop/src/main/java/it/niedermann/android/crosstabdnd/DragAndDropAdapter.java index 4484d04f0..a143ccaee 100644 --- a/cross-tab-drag-and-drop/src/main/java/it/niedermann/android/crosstabdnd/DragAndDropAdapter.java +++ b/cross-tab-drag-and-drop/src/main/java/it/niedermann/android/crosstabdnd/DragAndDropAdapter.java @@ -1,5 +1,7 @@ package it.niedermann.android.crosstabdnd; +import androidx.annotation.NonNull; + import java.util.List; public interface DragAndDropAdapter<Model> { @@ -10,5 +12,6 @@ public interface DragAndDropAdapter<Model> { void insertItem(Model item, int position); + @NonNull List<Model> getItemList(); } diff --git a/fastlane/metadata/android/en-US/changelogs/1007000.txt b/fastlane/metadata/android/en-US/changelogs/1007000.txt new file mode 100644 index 000000000..c6f510575 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/1007000.txt @@ -0,0 +1,11 @@ +1.7.0 + +- 🆕 Compact mode (#579) + +1.6.0 + +- ✨ Adjust design to new style of Nextcloud Android app (#525) +- 🎨 Brand now uses board color to provide better context (#525) +- 🎨 Offer the same default board colors as the server app +- 🔍 Display all available users as search results when sharing a board (#510) +- 🐞 "Archive cards" of list crashes the app when there is no list (#557)
\ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/1008000.txt b/fastlane/metadata/android/en-US/changelogs/1008000.txt new file mode 100644 index 000000000..ea0783dc0 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/1008000.txt @@ -0,0 +1,16 @@ +1.8.0 + +- 🔀 Move cards to other boards and accounts (#453) +- ➕ Clone boards with existing stacks and labels (#455) + +1.7.0 + +- 🆕 Compact mode (#579) + +1.6.0 + +- ✨ Adjust design to new style of Nextcloud Android app (#525) +- 🎨 Brand now uses board color to provide better context (#525) +- 🎨 Offer the same default board colors as the server app +- 🔍 Display all available users as search results when sharing a board (#510) +- 🐞 "Archive cards" of list crashes the app when there is no list (#557)
\ No newline at end of file diff --git a/fastlane/metadata/android/en-US/title.txt b/fastlane/metadata/android/en-US/title.txt index 59e07421a..3792d0591 100644 --- a/fastlane/metadata/android/en-US/title.txt +++ b/fastlane/metadata/android/en-US/title.txt @@ -1 +1 @@ -Nextcloud Deck for Android
\ No newline at end of file +Nextcloud Deck
\ No newline at end of file |