diff options
author | Stefan Niedermann <info@niedermann.it> | 2020-07-13 16:23:54 +0300 |
---|---|---|
committer | Stefan Niedermann <info@niedermann.it> | 2020-07-13 16:23:54 +0300 |
commit | 6403676b4f6f6a29c06aa2dfa9e502dc4b54c888 (patch) | |
tree | af5de454eadda978abc995a5d910ffbc0c00ab6a /app/src | |
parent | 4ad16e456e1b5dea50ade8ef973a8386493aa0c3 (diff) | |
parent | f8283d1a77aba90b45ec9d787606a973c4df6137 (diff) |
Merge branch 'master' into stack-widget
# Conflicts:
# app/src/main/res/values/strings.xml
Diffstat (limited to 'app/src')
66 files changed, 1486 insertions, 302 deletions
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> |