diff options
author | Stefan Niedermann <info@niedermann.it> | 2020-10-29 18:37:48 +0300 |
---|---|---|
committer | Stefan Niedermann <info@niedermann.it> | 2020-10-29 18:37:48 +0300 |
commit | 10421aa54f6a1ae013cfbd285335b67fbd4ba385 (patch) | |
tree | 0ee443e449d696d233d12f7142ccb6e7057254b4 /app/src | |
parent | 8326794c226256ee3236175bc499d0140f000e3c (diff) | |
parent | 2a2076cca20bdd6e059e0c0d51b369c6d43e7f17 (diff) |
Merge branch 'master' into 289-upload-sources
# Conflicts:
# app/src/main/AndroidManifest.xml
# app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentsFragment.java
# app/src/main/res/values/strings.xml
Diffstat (limited to 'app/src')
245 files changed, 8070 insertions, 2193 deletions
diff --git a/app/src/androidTest/java/it/niedermann/nextcloud/deck/util/ColorUtilTest.java b/app/src/androidTest/java/it/niedermann/nextcloud/deck/util/ColorUtilTest.java deleted file mode 100644 index afc09e762..000000000 --- a/app/src/androidTest/java/it/niedermann/nextcloud/deck/util/ColorUtilTest.java +++ /dev/null @@ -1,124 +0,0 @@ -package it.niedermann.nextcloud.deck.util; - -import android.graphics.Color; - -import androidx.annotation.ColorInt; -import androidx.core.util.Pair; -import androidx.test.ext.junit.runners.AndroidJUnit4; - -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.junit.runner.RunWith; - -import java.util.ArrayList; -import java.util.List; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -@RunWith(AndroidJUnit4.class) -public class ColorUtilTest { - - @ColorInt - private static final int[] DARK_COLORS = new int[]{ - Color.BLACK, - Color.parseColor("#0082C9"), // "Nextcloud-Blue" - Color.parseColor("#007676") - }; - - @ColorInt - private static final int[] LIGHT_COLORS = new int[]{ - Color.WHITE, - Color.YELLOW - }; - - @Test - public void testGetForegroundColorForBackgroundColor() { - for (@ColorInt int color : DARK_COLORS) { - assertEquals( - "Expect foreground color for " + String.format("#%06X", (0xFFFFFF & color)) + " to be " + String.format("#%06X", (0xFFFFFF & Color.WHITE)), - Color.WHITE, ColorUtil.getForegroundColorForBackgroundColor(color) - ); - } - for (@ColorInt int color : LIGHT_COLORS) { - assertEquals( - "Expect foreground color for " + String.format("#%06X", (0xFFFFFF & color)) + " to be " + String.format("#%06X", (0xFFFFFF & Color.BLACK)), - Color.BLACK, ColorUtil.getForegroundColorForBackgroundColor(color) - ); - } - assertEquals( - "Expect foreground color for " + String.format("#%06X", (0xFFFFFF & Color.TRANSPARENT)) + " to be " + String.format("#%06X", (0xFFFFFF & Color.BLACK)), - Color.BLACK, ColorUtil.getForegroundColorForBackgroundColor(Color.TRANSPARENT) - ); - } - - @Test - public void testIsColorDark() { - for (@ColorInt int color : DARK_COLORS) { - assertTrue( - "Expect " + String.format("#%06X", (0xFFFFFF & color)) + " to be a dark color", - ColorUtil.isColorDark(color) - ); - } - for (@ColorInt int color : LIGHT_COLORS) { - assertFalse( - "Expect " + String.format("#%06X", (0xFFFFFF & color)) + " to be a light color", - ColorUtil.isColorDark(color) - ); - } - } - - @Test - public void testContrastRatioIsSufficient() { - final List<Pair<Integer, Integer>> sufficientContrastColorPairs = new ArrayList<>(); - sufficientContrastColorPairs.add(new Pair<>(Color.BLACK, Color.WHITE)); - sufficientContrastColorPairs.add(new Pair<>(Color.WHITE, Color.parseColor("#0082C9"))); - - for (Pair<Integer, Integer> colorPair : sufficientContrastColorPairs) { - assert colorPair.first != null; - assert colorPair.second != null; - assertTrue( - "Expect contrast between " + String.format("#%06X", (0xFFFFFF & colorPair.first)) + " and " + String.format("#%06X", (0xFFFFFF & colorPair.second)) + " to be sufficient", - ColorUtil.contrastRatioIsSufficient(colorPair.first, colorPair.second) - ); - } - - final List<Pair<Integer, Integer>> insufficientContrastColorPairs = new ArrayList<>(); - insufficientContrastColorPairs.add(new Pair<>(Color.WHITE, Color.WHITE)); - insufficientContrastColorPairs.add(new Pair<>(Color.BLACK, Color.BLACK)); - - for (Pair<Integer, Integer> colorPair : insufficientContrastColorPairs) { - assert colorPair.first != null; - assert colorPair.second != null; - assertFalse( - "Expect contrast between " + String.format("#%06X", (0xFFFFFF & colorPair.first)) + " and " + String.format("#%06X", (0xFFFFFF & colorPair.second)) + " to be insufficient", - ColorUtil.contrastRatioIsSufficient(colorPair.first, colorPair.second) - ); - } - } - - @Rule - public final ExpectedException exception = ExpectedException.none(); - - @Test - public void testGetCleanHexaColorString() { - final List<Pair<String, String>> validColors = new ArrayList<>(); - validColors.add(new Pair<>("#0082C9", "#0082C9")); - validColors.add(new Pair<>("0082C9", "#0082C9")); - validColors.add(new Pair<>("#CCC", "#CCCCCC")); - validColors.add(new Pair<>("ccc", "#cccccc")); - validColors.add(new Pair<>("af0", "#aaff00")); - validColors.add(new Pair<>("#af0", "#aaff00")); - for (Pair<String, String> color : validColors) { - assertEquals("Expect " + color.first + " to be cleaned up to " + color.second, color.second, ColorUtil.formatColorToParsableHexString(color.first)); - } - - final String[] invalidColors = new String[]{null, "", "cc", "c", "#a", "#55L", "55L"}; - for (String color : invalidColors) { - exception.expect(IllegalArgumentException.class); - ColorUtil.formatColorToParsableHexString(color); - } - } -} diff --git a/app/src/androidTest/java/it/niedermann/nextcloud/deck/util/DeckColorUtilTest.java b/app/src/androidTest/java/it/niedermann/nextcloud/deck/util/DeckColorUtilTest.java new file mode 100644 index 000000000..10ac5cdc9 --- /dev/null +++ b/app/src/androidTest/java/it/niedermann/nextcloud/deck/util/DeckColorUtilTest.java @@ -0,0 +1,49 @@ +package it.niedermann.nextcloud.deck.util; + +import android.graphics.Color; + +import androidx.core.util.Pair; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +@RunWith(AndroidJUnit4.class) +public class DeckColorUtilTest { + + @Test + public void testContrastRatioIsSufficient() { + final List<Pair<Integer, Integer>> sufficientContrastColorPairs = new ArrayList<>(); + sufficientContrastColorPairs.add(new Pair<>(Color.BLACK, Color.WHITE)); + sufficientContrastColorPairs.add(new Pair<>(Color.WHITE, Color.parseColor("#0082C9"))); + + for (Pair<Integer, Integer> colorPair : sufficientContrastColorPairs) { + assert colorPair.first != null; + assert colorPair.second != null; + assertTrue( + "Expect contrast between " + String.format("#%06X", (0xFFFFFF & colorPair.first)) + " and " + String.format("#%06X", (0xFFFFFF & colorPair.second)) + " to be sufficient", + DeckColorUtil.contrastRatioIsSufficient(colorPair.first, colorPair.second) + ); + } + + final List<Pair<Integer, Integer>> insufficientContrastColorPairs = new ArrayList<>(); + insufficientContrastColorPairs.add(new Pair<>(Color.WHITE, Color.WHITE)); + insufficientContrastColorPairs.add(new Pair<>(Color.BLACK, Color.BLACK)); + + for (Pair<Integer, Integer> colorPair : insufficientContrastColorPairs) { + assert colorPair.first != null; + assert colorPair.second != null; + assertFalse( + "Expect contrast between " + String.format("#%06X", (0xFFFFFF & colorPair.first)) + " and " + String.format("#%06X", (0xFFFFFF & colorPair.second)) + " to be insufficient", + DeckColorUtil.contrastRatioIsSufficient(colorPair.first, colorPair.second) + ); + } + } + +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index af5f83305..609c022bb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,7 +3,6 @@ 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" /> @@ -76,7 +75,7 @@ <activity android:name=".ui.archivedboards.ArchivedBoardsActvitiy" android:label="@string/archived_boards" - android:parentActivityName="it.niedermann.nextcloud.deck.ui.MainActivity" /> + android:parentActivityName="it.niedermann.nextcloud.deck.ui.MainActivity" /> <activity android:name=".ui.card.EditActivity" @@ -101,7 +100,7 @@ <activity android:name=".ui.preparecreate.PrepareCreateActivity" android:description="@string/add_a_new_card_using_the_button" - android:label="@string/add_card" > + android:label="@string/add_card"> <intent-filter> <action android:name="android.intent.action.SEND" /> @@ -117,7 +116,7 @@ <activity android:name=".ui.PushNotificationActivity" - android:label="@string/app_name" > + android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.MAIN" /> </intent-filter> @@ -127,6 +126,25 @@ android:name=".ui.exception.ExceptionActivity" android:process=":error_activity" /> + <activity android:name=".ui.widget.stack.StackWidgetConfigurationActivity"> + <intent-filter> + <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" /> + </intent-filter> + </activity> + + <receiver + android:name="it.niedermann.nextcloud.deck.ui.widget.stack.StackWidget" + android:label="@string/widget_stack_title"> + + <intent-filter> + <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> + </intent-filter> + + <meta-data + android:name="android.appwidget.provider" + android:resource="@xml/stack_widget_provider" /> + </receiver> + <service android:name=".ui.tiles.EditCardTileService" android:description="@string/add_a_new_card_using_the_button" @@ -138,6 +156,10 @@ </intent-filter> </service> + <service + android:name=".ui.widget.stack.StackWidgetService" + android:permission="android.permission.BIND_REMOTEVIEWS" /> + <activity android:name=".ui.widget.singlecard.SelectCardForWidgetActivity" android:label="@string/share_add_to_card" @@ -148,7 +170,9 @@ </intent-filter> </activity> - <receiver android:name=".ui.widget.singlecard.SingleCardWidget"> + <receiver + android:name=".ui.widget.singlecard.SingleCardWidget" + android:label="@string/single_card"> <intent-filter> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> </intent-filter> diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/DeckApplication.java b/app/src/main/java/it/niedermann/nextcloud/deck/DeckApplication.java index ff492c226..cf30a0971 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/DeckApplication.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/DeckApplication.java @@ -1,20 +1,17 @@ package it.niedermann.nextcloud.deck; -import android.app.Application; import android.content.Context; import android.content.SharedPreferences; import androidx.annotation.NonNull; +import androidx.multidex.MultiDexApplication; import androidx.preference.PreferenceManager; -import com.jakewharton.threetenabp.AndroidThreeTen; - import static androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_NO; import static androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_YES; import static androidx.appcompat.app.AppCompatDelegate.setDefaultNightMode; -import static androidx.multidex.MultiDex.install; -public class DeckApplication extends Application { +public class DeckApplication extends MultiDexApplication { public static final long NO_ACCOUNT_ID = -1L; public static final long NO_BOARD_ID = -1L; @@ -24,17 +21,6 @@ public class DeckApplication extends Application { public void onCreate() { setAppTheme(isDarkTheme(getApplicationContext())); super.onCreate(); - AndroidThreeTen.init(this); - } - - // -------- - // Multidex - // -------- - - @Override - protected void attachBaseContext(Context base) { - super.attachBaseContext(base); - install(this); } // ----------------- diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/api/DeckAPI.java b/app/src/main/java/it/niedermann/nextcloud/deck/api/DeckAPI.java index 9883608c2..a303a8a33 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/api/DeckAPI.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/api/DeckAPI.java @@ -83,11 +83,11 @@ public interface DeckAPI { @FormUrlEncoded @PUT("boards/{boardId}/stacks/{stackId}/cards/{cardId}/assignLabel") - Observable assignLabelToCard(@Path("boardId") long boardId, @Path("stackId") long stackId, @Path("cardId") long cardId, @Field("labelId") long labelId); + Observable<Void> assignLabelToCard(@Path("boardId") long boardId, @Path("stackId") long stackId, @Path("cardId") long cardId, @Field("labelId") long labelId); @FormUrlEncoded @PUT("boards/{boardId}/stacks/{stackId}/cards/{cardId}/removeLabel") - Observable unassignLabelFromCard(@Path("boardId") long boardId, @Path("stackId") long stackId, @Path("cardId") long cardId, @Field("labelId") long labelId); + Observable<Void> unassignLabelFromCard(@Path("boardId") long boardId, @Path("stackId") long stackId, @Path("cardId") long cardId, @Field("labelId") long labelId); @FormUrlEncoded @PUT("boards/{boardId}/stacks/{stackId}/cards/{cardId}/assignUser") 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..5da408e44 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 @@ -16,6 +16,9 @@ import it.niedermann.nextcloud.deck.model.full.FullStack; import it.niedermann.nextcloud.deck.model.ocs.Activity; import it.niedermann.nextcloud.deck.model.ocs.Capabilities; import it.niedermann.nextcloud.deck.model.ocs.comment.OcsComment; +import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProjectList; +import it.niedermann.nextcloud.deck.model.ocs.user.GroupMemberUIDs; +import it.niedermann.nextcloud.deck.model.ocs.user.OcsUser; import it.niedermann.nextcloud.deck.model.ocs.user.OcsUserList; /** @@ -39,10 +42,14 @@ public class GsonConfig { Type stack = new TypeToken<FullStack>() {}.getType(); Type capabilities = new TypeToken<Capabilities>() {}.getType(); Type ocsUserList = new TypeToken<OcsUserList>() {}.getType(); + Type ocsUser = new TypeToken<OcsUser>() {}.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(); + Type projectList = new TypeToken<OcsProjectList>() {}.getType(); + Type groupMembers = new TypeToken<GroupMemberUIDs>() {}.getType(); INSTANCE = new GsonBuilder() .setDateFormat(DATE_PATTERN) @@ -58,10 +65,14 @@ public class GsonConfig { .registerTypeAdapter(stack, new NextcloudDeserializer<>("stack", FullStack.class)) .registerTypeAdapter(capabilities, new NextcloudDeserializer<>("capability", Capabilities.class)) .registerTypeAdapter(ocsUserList, new NextcloudDeserializer<>("ocsUserList", OcsUserList.class)) + .registerTypeAdapter(ocsUser, new NextcloudDeserializer<>("ocsUser", OcsUser.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)) + .registerTypeAdapter(projectList, new NextcloudDeserializer<>("projectList", OcsProjectList.class)) + .registerTypeAdapter(groupMembers, new NextcloudDeserializer<>("groupMembers", GroupMemberUIDs.class)) .create(); } 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..6a8fcb873 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 @@ -1,20 +1,20 @@ package it.niedermann.nextcloud.deck.api; +import android.graphics.Color; + import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; -import org.threeten.bp.DateTimeUtils; -import org.threeten.bp.ZonedDateTime; -import org.threeten.bp.format.DateTimeFormatter; - +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Date; import java.util.List; +import it.niedermann.android.util.ColorUtil; 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; @@ -32,10 +32,15 @@ import it.niedermann.nextcloud.deck.model.ocs.Version; import it.niedermann.nextcloud.deck.model.ocs.comment.DeckComment; import it.niedermann.nextcloud.deck.model.ocs.comment.Mention; import it.niedermann.nextcloud.deck.model.ocs.comment.OcsComment; +import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProject; +import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProjectList; +import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProjectResource; +import it.niedermann.nextcloud.deck.model.ocs.user.GroupMemberUIDs; 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 { @@ -54,18 +59,42 @@ public class JsonToEntityParser { return (T) parseCapabilities(obj); } else if (mType == OcsUserList.class) { return (T) parseOcsUserList(obj); + } else if (mType == OcsUser.class) { + return (T) parseSingleOcsUser(obj); } else if (mType == Attachment.class) { return (T) parseAttachment(obj); } else if (mType == OcsComment.class) { return (T) parseOcsComment(obj); + } else if (mType == GroupMemberUIDs.class) { + return (T) parseGroupMemberUIDs(obj); + } else if (mType == OcsProjectList.class) { + return (T) parseOcsProjectList(obj); } throw new IllegalArgumentException("unregistered type: " + mType.getCanonicalName()); } + private static GroupMemberUIDs parseGroupMemberUIDs(JsonObject obj) { + DeckLog.verbose(obj.toString()); + GroupMemberUIDs uids = new GroupMemberUIDs(); + makeTraceableIfFails(() -> { + JsonElement data = obj.get("ocs").getAsJsonObject().get("data"); + if (!data.isJsonNull() && data.getAsJsonObject().has("users")) { + JsonElement users = data.getAsJsonObject().get("users"); + if (!users.isJsonNull() && users.isJsonArray()) { + for (JsonElement userElement : users.getAsJsonArray()) { + uids.add(userElement.getAsString()); + } + } + } + + }, obj); + return uids; + } + 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"); @@ -87,10 +116,105 @@ public class JsonToEntityParser { return ocsUserList; } + private static OcsUser parseSingleOcsUser(JsonObject obj) { + DeckLog.verbose(obj.toString()); + OcsUser ocsUser = new OcsUser(); + makeTraceableIfFails(() -> { + JsonElement data = obj.get("ocs").getAsJsonObject().get("data"); + if (!data.isJsonNull()) { + JsonObject user = data.getAsJsonObject(); + if (user.has("id")) { + ocsUser.setId(user.get("id").getAsString()); + } + if (user.has("displayname")) { + ocsUser.setDisplayName(user.get("displayname").getAsString()); + } + } + + }, obj); + return ocsUser; + } + + private static OcsProjectList parseOcsProjectList(JsonObject obj) { + DeckLog.verbose(obj.toString()); + OcsProjectList projectList = new OcsProjectList(); + makeTraceableIfFails(() -> { + JsonElement data = obj.get("ocs").getAsJsonObject().get("data"); + if (!data.isJsonNull() && data.isJsonArray()) { + JsonArray projectJsonArray = data.getAsJsonArray(); + for (JsonElement jsonArrayElement : projectJsonArray) { + if (jsonArrayElement.isJsonObject()) { + JsonObject jsonObject = jsonArrayElement.getAsJsonObject(); + OcsProject project = new OcsProject(); + project.setId(jsonObject.get("id").getAsLong()); + project.setName(getNullAsEmptyString(jsonObject.get("name"))); + project.setResources(new ArrayList<>()); + JsonElement jsonResources = jsonObject.get("resources"); + if (jsonResources != null && jsonResources.isJsonArray()) { + JsonArray resourcesArray = jsonResources.getAsJsonArray(); + for (JsonElement resourceElement : resourcesArray) { + if (resourceElement.isJsonObject()) { + OcsProjectResource resource = parseOcsProjectResource(resourceElement.getAsJsonObject()); + resource.setProjectId(project.getId()); + project.getResources().add(resource); + } + } + } + projectList.add(project); + } + } + } + + }, obj); + return projectList; + } + + private static OcsProjectResource parseOcsProjectResource(JsonObject obj) { + DeckLog.verbose(obj.toString()); + OcsProjectResource resource = new OcsProjectResource(); + makeTraceableIfFails(() -> { + if (obj.has("id")) { + String idString = obj.get("id").getAsString(); + if (idString != null && idString.trim().length() > 0) { + if (idString.matches("[0-9]+")) { + resource.setId(Long.parseLong(idString.trim())); + } else { + resource.setIdString(idString); + } + } + } + if (obj.has("type")) { + resource.setType(getNullAsEmptyString(obj.get("type"))); + } + if (obj.has("name")) { + resource.setName(getNullAsEmptyString(obj.get("name"))); + } + if (obj.has("link")) { + resource.setLink(getNullAsEmptyString(obj.get("link"))); + } + if (obj.has("iconUrl")) { + resource.setIconUrl(getNullAsEmptyString(obj.get("iconUrl"))); + } + if (obj.has("path")) { + resource.setPath(obj.get("path").getAsString()); + } + if (obj.has("mimetype")) { + resource.setMimetype(obj.get("mimetype").getAsString()); + } + if (obj.has("preview-available")) { + resource.setPreviewAvailable(obj.get("preview-available").getAsBoolean()); + } else { + resource.setPreviewAvailable(false); + } + + }, obj); + return resource; + } + 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 +231,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()); @@ -118,17 +242,18 @@ public class JsonToEntityParser { deckComment.setActorType(commentJson.get("actorType").getAsString()); deckComment.setCreationDateTime(getTimestampFromString(commentJson.get("creationDateTime"))); - if (commentJson.has("replyTo")){ - JsonObject replyTo = commentJson.get("replyTo").getAsJsonObject(); - deckComment.setParentId(replyTo.get("id").getAsLong()); - } + if (commentJson.has("replyTo")) { + JsonObject replyTo = commentJson.get("replyTo").getAsJsonObject(); + deckComment.setParentId(replyTo.get("id").getAsLong()); + } - JsonElement mentions = commentJson.get("mentions"); - if (mentions != null && mentions.isJsonArray()) { - for (JsonElement mention : mentions.getAsJsonArray()) { - deckComment.addMention(parseMention(mention)); + JsonElement mentions = commentJson.get("mentions"); + if (mentions != null && mentions.isJsonArray()) { + for (JsonElement mention : mentions.getAsJsonArray()) { + deckComment.addMention(parseMention(mention)); + } } - }}, data); + }, data); return deckComment; } @@ -138,7 +263,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 +280,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()); @@ -218,12 +343,19 @@ public class JsonToEntityParser { } } - JsonElement owner = e.get("owner"); - if (owner != null) { - if (owner.isJsonPrimitive()) {//TODO: remove if, let only else! - DeckLog.verbose("owner is Primitive, skipping"); - } else - fullBoard.setOwner(parseUser(owner.getAsJsonObject())); + if (e.has("owner")) { + fullBoard.setOwner(parseUser(e.get("owner"))); + } + if (e.has("users")) { + JsonElement users = e.get("users"); + if (users != null && !users.isJsonNull() && users.isJsonArray()) { + JsonArray usersArray = users.getAsJsonArray(); + List<User> usersList = new ArrayList<>(); + for (JsonElement userJson : usersArray) { + usersList.add(parseUser(userJson)); + } + fullBoard.setUsers(usersList); + } } }, e); return fullBoard; @@ -234,8 +366,8 @@ public class JsonToEntityParser { AccessControl acl = new AccessControl(); if (aclJson.has("participant") && !aclJson.get("participant").isJsonNull()) { - TraceableException.makeTraceableIfFails(() -> { - User participant = parseUser(aclJson.get("participant").getAsJsonObject()); + makeTraceableIfFails(() -> { + User participant = parseUser(aclJson.get("participant")); acl.setUser(participant); acl.setType(aclJson.get("type").getAsLong()); acl.setBoardId(aclJson.get("boardId").getAsLong()); @@ -257,7 +389,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"))); @@ -282,7 +414,7 @@ public class JsonToEntityParser { for (JsonElement assignedUser : assignedUsers) { JsonObject userJson = assignedUser.getAsJsonObject(); if (userJson.has("participant") && !userJson.get("participant").isJsonNull()) { - users.add(parseUser(userJson.get("participant").getAsJsonObject())); + users.add(parseUser(userJson.get("participant"))); } } fullCard.setAssignedUsers(users); @@ -308,10 +440,7 @@ public class JsonToEntityParser { card.setCommentsUnread(e.get("commentsUnread").getAsInt()); JsonElement owner = e.get("owner"); if (owner != null) { - if (owner.isJsonPrimitive()) {//TODO: remove if, let only else! - DeckLog.verbose("owner is Primitive, skipping"); - } else - fullCard.setOwner(parseUser(owner.getAsJsonObject())); + fullCard.setOwner(parseUser(owner)); } card.setArchived(e.get("archived").getAsBoolean()); }, e); @@ -322,7 +451,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()); @@ -331,7 +460,7 @@ public class JsonToEntityParser { a.setCreatedAt(getTimestampFromLong(e.get("createdAt"))); a.setCreatedBy(e.get("createdBy").getAsString()); a.setDeletedAt(getTimestampFromLong(e.get("deletedAt"))); - if (e.has("extendedData") && !e.get("extendedData").isJsonNull()) { + if (e.has("extendedData") && !e.get("extendedData").isJsonNull() && e.get("extendedData").isJsonObject()) { JsonObject extendedData = e.getAsJsonObject("extendedData").getAsJsonObject(); a.setFilesize(extendedData.get("filesize").getAsLong()); a.setMimetype(extendedData.get("mimetype").getAsString()); @@ -350,14 +479,27 @@ public class JsonToEntityParser { return a; } - protected static User parseUser(JsonObject e) { - DeckLog.verbose(e.toString()); + protected static User parseUser(JsonElement userElement) { + DeckLog.verbose(userElement.toString()); + if (userElement.isJsonNull()) { + return null; + } User user = new User(); - TraceableException.makeTraceableIfFails(() -> { - user.setDisplayname(getNullAsEmptyString(e.get("displayname"))); - user.setPrimaryKey(getNullAsEmptyString(e.get("primaryKey"))); - user.setUid(getNullAsEmptyString(e.get("uid"))); - }, e); + makeTraceableIfFails(() -> { + + if (userElement.isJsonPrimitive()) { + String uid = userElement.getAsString(); + user.setDisplayname(uid); + user.setPrimaryKey(uid); + user.setUid(uid); + } else { + JsonObject userJson = userElement.getAsJsonObject(); + user.setDisplayname(getNullAsEmptyString(userJson.get("displayname"))); + user.setPrimaryKey(getNullAsEmptyString(userJson.get("primaryKey"))); + user.setUid(getNullAsEmptyString(userJson.get("uid"))); + } + + }, userElement); return user; } @@ -405,8 +547,8 @@ public class JsonToEntityParser { } if (caps.has("theming")) { JsonObject theming = caps.getAsJsonObject("theming"); - capabilities.setColor(theming.get("color").getAsString()); - capabilities.setTextColor(theming.get("color-text").getAsString()); + capabilities.setColor(getColorAsInt(theming, "color")); + capabilities.setTextColor(getColorAsInt(theming, "color-text")); } } capabilities.setDeckVersion(Version.of(version)); @@ -415,11 +557,24 @@ public class JsonToEntityParser { return capabilities; } + private static int getColorAsInt(JsonObject element, String field) { + String rawString = getNullAsEmptyString(element.get(field)); + try { + if (!rawString.trim().isEmpty()) { + String colorAsString = ColorUtil.INSTANCE.formatColorToParsableHexString(rawString); + return Color.parseColor(colorAsString); + } + } catch (Exception e) { + // Do mostly nothing, return default value + } + return Color.GRAY; + } + protected static List<Activity> parseActivity(JsonObject e) { 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 +602,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,12 +629,12 @@ 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); label.setTitle(getNullAsEmptyString(e.get("title"))); - label.setColor(getNullAsEmptyString(e.get("color"))); + label.setColor(getColorAsInt(e, "color")); }, e); return label; } @@ -493,7 +648,7 @@ public class JsonToEntityParser { return null; } else { String dateAsString = jsonElement.getAsString(); - return DateTimeUtils.toDate(ZonedDateTime.from(DateTimeFormatter.ISO_DATE_TIME.parse(dateAsString)).toInstant()); + return new Date(ZonedDateTime.from(DateTimeFormatter.ISO_DATE_TIME.parse(dateAsString)).toInstant().toEpochMilli()); } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/api/NextcloudServerAPI.java b/app/src/main/java/it/niedermann/nextcloud/deck/api/NextcloudServerAPI.java index 67a70aba4..7b93ace49 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/api/NextcloudServerAPI.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/api/NextcloudServerAPI.java @@ -1,6 +1,8 @@ package it.niedermann.nextcloud.deck.api; +import com.nextcloud.android.sso.api.ParsedResponse; + import java.util.List; import io.reactivex.Observable; @@ -8,10 +10,14 @@ import it.niedermann.nextcloud.deck.model.ocs.Activity; 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.projects.OcsProjectList; +import it.niedermann.nextcloud.deck.model.ocs.user.GroupMemberUIDs; +import it.niedermann.nextcloud.deck.model.ocs.user.OcsUser; import it.niedermann.nextcloud.deck.model.ocs.user.OcsUserList; import retrofit2.http.Body; import retrofit2.http.DELETE; import retrofit2.http.GET; +import retrofit2.http.Header; import retrofit2.http.Headers; import retrofit2.http.POST; import retrofit2.http.PUT; @@ -21,11 +27,20 @@ import retrofit2.http.Query; public interface NextcloudServerAPI { @GET("cloud/capabilities?format=json") - Observable<Capabilities> getCapabilities(); + Observable<ParsedResponse<Capabilities>> getCapabilities(@Header("If-None-Match") String eTag); + + @GET("collaboration/resources/deck-card/{cardId}?format=json") + Observable<OcsProjectList> getProjectsForCard(@Path("cardId") long cardId); @GET("apps/files_sharing/api/v1/sharees?format=json&perPage=20&itemType=0%2C1%2C7") Observable<OcsUserList> searchUser(@Query("search") String searchTerm); + @GET("cloud/groups/{search}?format=json") + Observable<GroupMemberUIDs> searchGroupMembers(@Path("search") String groupUid); + + @GET("cloud/users/{search}?format=json") + Observable<OcsUser> getSingleUserData(@Path("search") String userUid); + @GET("apps/activity/api/v2/activity/filter?format=json&object_type=deck_card&limit=50&since=-1&sort=asc") Observable<List<Activity>> getActivitiesForCard(@Query("object_id") long cardId); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/api/json/JsonColorSerializer.java b/app/src/main/java/it/niedermann/nextcloud/deck/api/json/JsonColorSerializer.java new file mode 100644 index 000000000..5169e7f5e --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/api/json/JsonColorSerializer.java @@ -0,0 +1,26 @@ +package it.niedermann.nextcloud.deck.api.json; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +import java.io.IOException; + +import it.niedermann.android.util.ColorUtil; + +public class JsonColorSerializer extends TypeAdapter<Integer> { + @Override + public void write(JsonWriter out, Integer value) throws IOException { + if (value == null) { + out.nullValue(); + } else { + out.value(ColorUtil.INSTANCE.intColorToHexString(value)); + } + } + + @Override + public Integer read(JsonReader in) throws IOException { + // currently not needed + return null; + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/exceptions/DeckException.java b/app/src/main/java/it/niedermann/nextcloud/deck/exceptions/DeckException.java index 8c2904d2b..d9d8c5dde 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/exceptions/DeckException.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/exceptions/DeckException.java @@ -5,6 +5,7 @@ public class DeckException extends IllegalArgumentException { public enum Hint { CAPABILITIES_NOT_PARSABLE, CAPABILITIES_VERSION_NOT_PARSABLE, + UNKNOWN_ACCOUNT_USER_ID } private Hint hint; 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/AccessControl.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/AccessControl.java index 299dd7ca0..e2a3ccc3b 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/model/AccessControl.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/AccessControl.java @@ -8,6 +8,7 @@ import androidx.room.Index; import com.google.gson.annotations.SerializedName; import it.niedermann.nextcloud.deck.model.interfaces.AbstractRemoteEntity; +import it.niedermann.nextcloud.deck.model.ocs.user.GroupMemberUIDs; @Entity(inheritSuperIndices = true, indices = { @@ -35,6 +36,8 @@ public class AccessControl extends AbstractRemoteEntity { @Ignore @SerializedName("participant") private User user; + @Ignore + private GroupMemberUIDs groupMemberUIDs; public AccessControl() { super(); @@ -115,6 +118,14 @@ public class AccessControl extends AbstractRemoteEntity { this.user = user; } + public GroupMemberUIDs getGroupMemberUIDs() { + return groupMemberUIDs; + } + + public void setGroupMemberUIDs(GroupMemberUIDs groupMemberUIDs) { + this.groupMemberUIDs = groupMemberUIDs; + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/Account.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/Account.java index ba8fb72b7..1c189d162 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/model/Account.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/Account.java @@ -2,6 +2,7 @@ package it.niedermann.nextcloud.deck.model; import android.net.Uri; +import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Px; import androidx.room.ColumnInfo; @@ -18,7 +19,6 @@ import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.model.ocs.Capabilities; import it.niedermann.nextcloud.deck.model.ocs.Version; import it.niedermann.nextcloud.deck.ui.accountswitcher.AccountSwitcherDialog; -import it.niedermann.nextcloud.deck.util.ColorUtil; @Entity(indices = {@Index(value = "name", unique = true)}) public class Account implements Serializable { @@ -38,12 +38,12 @@ public class Account implements Serializable { private String url; @NonNull - @ColumnInfo(defaultValue = "#0082c9") - private String color = "#0082c9"; + @ColumnInfo(defaultValue = "0") + private Integer color = 0; @NonNull - @ColumnInfo(defaultValue = "#ffffff") - private String textColor = "#ffffff"; + @ColumnInfo(defaultValue = "0") + private Integer textColor = 0; @NonNull @ColumnInfo(defaultValue = "0.6.4") @@ -53,6 +53,8 @@ public class Account implements Serializable { @ColumnInfo(defaultValue = "0") private boolean maintenanceEnabled = false; + private String etag; + @Ignore public Account(Long id, @NonNull String name, @NonNull String userName, @NonNull String url) { this(name, userName, url); @@ -60,7 +62,7 @@ public class Account implements Serializable { } @Ignore - public Account(String name, String userName, String url) { + public Account(@NonNull String name, @NonNull String userName, @NonNull String url) { this.name = name; this.userName = userName; this.url = url; @@ -74,22 +76,29 @@ public class Account implements Serializable { public Account() { } - public void applyCapabilities(Capabilities capabilities) { + public void applyCapabilities(Capabilities capabilities, String eTag) { + if (capabilities == null) { + maintenanceEnabled = true; + return; + } maintenanceEnabled = capabilities.isMaintenanceEnabled(); if (!isMaintenanceEnabled()) { try { // Nextcloud might return color format #000 which cannot be parsed by Color.parseColor() // https://github.com/stefan-niedermann/nextcloud-deck/issues/466 - color = ColorUtil.formatColorToParsableHexString(capabilities.getColor()); - textColor = ColorUtil.formatColorToParsableHexString(capabilities.getTextColor()); + color = capabilities.getColor(); + textColor = capabilities.getTextColor(); } catch (Exception e) { DeckLog.logError(e); - color = "#0082c9"; - color = "#ffffff"; + color = 0; + color = 0; } if (capabilities.getDeckVersion() != null) { serverDeckVersion = capabilities.getDeckVersion().getOriginalVersion(); } + if (eTag != null) { + this.etag = eTag; + } } } @@ -132,21 +141,23 @@ public class Account implements Serializable { return serialVersionUID; } + @ColorInt @NonNull - public String getColor() { + public Integer getColor() { return color; } - public void setColor(@NonNull String color) { + public void setColor(@NonNull Integer color) { this.color = color; } @NonNull - public String getTextColor() { + public Integer getTextColor() { return textColor; } - public void setTextColor(@NonNull String textColor) { + @Deprecated + public void setTextColor(@NonNull Integer textColor) { this.textColor = textColor; } @@ -171,6 +182,23 @@ public class Account implements Serializable { this.maintenanceEnabled = maintenanceEnabled; } + public String getEtag() { + return etag; + } + + public void setEtag(String etag) { + this.etag = etag; + } + + /** + * A cache buster parameter is added for duplicate account names on different hosts which shall be fetched from the same {@link SingleSignOnAccount} (e. g. {@link AccountSwitcherDialog}) + * + * @return an {@link String} to fetch the avatar for this account. + */ + public String getAvatarUrl(@Px int size) { + return getUrl() + "/index.php/avatar/" + Uri.encode(getUserName()) + "/" + size; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -185,16 +213,8 @@ public class Account implements Serializable { if (!url.equals(account.url)) return false; if (!color.equals(account.color)) return false; if (!textColor.equals(account.textColor)) return false; - return serverDeckVersion.equals(account.serverDeckVersion); - } - - /** - * A cache buster parameter is added for duplicate account names on different hosts which shall be fetched from the same {@link SingleSignOnAccount} (e. g. {@link AccountSwitcherDialog}) - * - * @return an {@link String} to fetch the avatar for this account. - */ - public String getAvatarUrl(@Px int size) { - return getUrl() + "/index.php/avatar/" + Uri.encode(getUserName()) + "/" + size; + if (!serverDeckVersion.equals(account.serverDeckVersion)) return false; + return etag != null ? etag.equals(account.etag) : account.etag == null; } @Override @@ -207,6 +227,7 @@ public class Account implements Serializable { result = 31 * result + textColor.hashCode(); result = 31 * result + serverDeckVersion.hashCode(); result = 31 * result + (maintenanceEnabled ? 1 : 0); + result = 31 * result + (etag != null ? etag.hashCode() : 0); return result; } @@ -221,6 +242,7 @@ public class Account implements Serializable { ", textColor='" + textColor + '\'' + ", serverDeckVersion='" + serverDeckVersion + '\'' + ", maintenanceEnabled=" + maintenanceEnabled + + ", eTag='" + etag + '\'' + '}'; } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/Board.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/Board.java index 29afe844b..c5cf7c843 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/model/Board.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/Board.java @@ -1,17 +1,23 @@ package it.niedermann.nextcloud.deck.model; +import android.graphics.Color; + +import androidx.annotation.ColorInt; import androidx.room.Entity; import androidx.room.ForeignKey; import androidx.room.Ignore; import androidx.room.Index; +import com.google.gson.annotations.JsonAdapter; + import java.io.Serializable; import java.util.Date; +import it.niedermann.android.util.ColorUtil; import it.niedermann.nextcloud.deck.DeckLog; +import it.niedermann.nextcloud.deck.api.json.JsonColorSerializer; import it.niedermann.nextcloud.deck.model.enums.DBStatus; import it.niedermann.nextcloud.deck.model.interfaces.AbstractRemoteEntity; -import it.niedermann.nextcloud.deck.util.ColorUtil; @Entity( inheritSuperIndices = true, @@ -38,10 +44,8 @@ public class Board extends AbstractRemoteEntity implements Serializable { private String title; private long ownerId; - /** - * Deck App sends color strings without leading # character - */ - private String color; + @JsonAdapter(JsonColorSerializer.class) + private Integer color; private boolean archived; private int shared; private Date deletedAt; @@ -79,21 +83,25 @@ public class Board extends AbstractRemoteEntity implements Serializable { this.id = id; } - public String getColor() { + @ColorInt + public Integer getColor() { return color; } + public void setColor(String color) { try { - // Nextcloud might return color format #000 which cannot be parsed by Color.parseColor() - // https://github.com/stefan-niedermann/nextcloud-deck/issues/466 - this.color = ColorUtil.formatColorToParsableHexString(color).substring(1); + setColor(Color.parseColor(ColorUtil.INSTANCE.formatColorToParsableHexString(color))); } catch (Exception e) { DeckLog.logError(e); - this.color = "757575"; + setColor(Color.GRAY); } } + public void setColor(Integer color) { + this.color = color; + } + public boolean isArchived() { return archived; } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/Card.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/Card.java index 732ccfc50..d0ba63f9a 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/model/Card.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/Card.java @@ -31,7 +31,7 @@ import it.niedermann.nextcloud.deck.model.interfaces.AbstractRemoteEntity; public class Card extends AbstractRemoteEntity { private static Pattern PATTERN_MD_TASK = Pattern.compile("\\[([xX ])]"); - public class TaskStatus { + public static class TaskStatus { public int taskCount; public int doneCount; diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/Label.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/Label.java index 0d97ce5a0..da24edb5a 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/model/Label.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/Label.java @@ -1,27 +1,41 @@ package it.niedermann.nextcloud.deck.model; +import android.graphics.Color; + +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; +import androidx.room.ColumnInfo; import androidx.room.Entity; import androidx.room.ForeignKey; import androidx.room.Index; +import com.google.gson.annotations.JsonAdapter; + import java.io.Serializable; +import it.niedermann.android.util.ColorUtil; +import it.niedermann.nextcloud.deck.DeckLog; +import it.niedermann.nextcloud.deck.api.json.JsonColorSerializer; import it.niedermann.nextcloud.deck.model.interfaces.AbstractRemoteEntity; @Entity(inheritSuperIndices = true, indices = {@Index("boardId"), @Index(value = {"boardId", "title"}, unique = true, name = "idx_label_title_unique")}, foreignKeys = { - @ForeignKey( - entity = Board.class, - parentColumns = "localId", - childColumns = "boardId", - onDelete = ForeignKey.CASCADE - ) - } + @ForeignKey( + entity = Board.class, + parentColumns = "localId", + childColumns = "boardId", + onDelete = ForeignKey.CASCADE + ) + } ) public class Label extends AbstractRemoteEntity implements Serializable { private String title; - private String color; + + @JsonAdapter(JsonColorSerializer.class) + @NonNull + @ColumnInfo(defaultValue = "0") + private Integer color; private long boardId; public Label() { @@ -42,14 +56,25 @@ public class Label extends AbstractRemoteEntity implements Serializable { this.title = title; } - public String getColor() { + @NonNull + @ColorInt + public Integer getColor() { return color; } - public void setColor(String color) { + public void setColor(@NonNull @ColorInt Integer color) { this.color = color; } + public void setColor(String color) { + try { + setColor(Color.parseColor(ColorUtil.INSTANCE.formatColorToParsableHexString(color))); + } catch (Exception e) { + DeckLog.logError(e); + setColor(Color.GRAY); + } + } + public long getBoardId() { return boardId; } @@ -62,18 +87,20 @@ public class Label extends AbstractRemoteEntity implements Serializable { public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; Label label = (Label) o; if (boardId != label.boardId) return false; if (title != null ? !title.equals(label.title) : label.title != null) return false; - return color != null ? color.equals(label.color) : label.color == null; + return color.equals(label.color); } @Override public int hashCode() { - int result = title != null ? title.hashCode() : 0; - result = 31 * result + (color != null ? color.hashCode() : 0); + int result = super.hashCode(); + result = 31 * result + (title != null ? title.hashCode() : 0); + result = 31 * result + color.hashCode(); result = 31 * result + (int) (boardId ^ (boardId >>> 32)); return result; } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/Stack.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/Stack.java index 330ec16d7..122b9ea30 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/model/Stack.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/Stack.java @@ -1,6 +1,5 @@ package it.niedermann.nextcloud.deck.model; -import androidx.annotation.NonNull; import androidx.room.Entity; import androidx.room.ForeignKey; import androidx.room.Ignore; @@ -35,12 +34,10 @@ public class Stack extends AbstractRemoteEntity { private String title; - @NonNull private long boardId; private Date deletedAt; - @NonNull private int order; // // @ToMany diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/User.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/User.java index cfb48d543..fd318ce7a 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/model/User.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/User.java @@ -4,10 +4,12 @@ import androidx.room.Entity; import androidx.room.Ignore; import androidx.room.Index; +import java.io.Serializable; + import it.niedermann.nextcloud.deck.model.interfaces.AbstractRemoteEntity; @Entity(inheritSuperIndices = true, indices = {@Index(value = "uid", name = "user_uid")}) -public class User extends AbstractRemoteEntity { +public class User extends AbstractRemoteEntity implements Serializable { private String primaryKey; diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/appwidgets/StackWidgetModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/appwidgets/StackWidgetModel.java new file mode 100644 index 000000000..fb7a74076 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/appwidgets/StackWidgetModel.java @@ -0,0 +1,68 @@ +package it.niedermann.nextcloud.deck.model.appwidgets; + +import androidx.room.Entity; +import androidx.room.ForeignKey; +import androidx.room.Index; +import androidx.room.PrimaryKey; + +import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.model.Stack; + +@Entity( + indices = { + @Index("stackId"), + @Index("accountId") + }, + foreignKeys = { + @ForeignKey( + entity = Account.class, + parentColumns = "id", + childColumns = "accountId", onDelete = ForeignKey.CASCADE + ), + @ForeignKey( + entity = Stack.class, + parentColumns = "localId", + childColumns = "stackId", onDelete = ForeignKey.CASCADE + ) + } +) +public class StackWidgetModel { + + @PrimaryKey() + private Integer appWidgetId; + private Long accountId; + private Long stackId; + private boolean darkTheme; + + public Integer getAppWidgetId() { + return appWidgetId; + } + + public void setAppWidgetId(Integer appWidgetId) { + this.appWidgetId = appWidgetId; + } + + public Long getAccountId() { + return accountId; + } + + public void setAccountId(Long accountId) { + this.accountId = accountId; + } + + public Long getStackId() { + return stackId; + } + + public void setStackId(Long stackId) { + this.stackId = stackId; + } + + public boolean getDarkTheme() { + return darkTheme; + } + + public void setDarkTheme(boolean darkTheme) { + this.darkTheme = darkTheme; + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/full/FullBoard.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/full/FullBoard.java index 31ad21f16..f9f1643ff 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/model/full/FullBoard.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/full/FullBoard.java @@ -1,5 +1,6 @@ package it.niedermann.nextcloud.deck.model.full; +import androidx.annotation.NonNull; import androidx.room.Embedded; import androidx.room.Ignore; import androidx.room.Relation; @@ -29,6 +30,8 @@ public class FullBoard implements IRemoteEntity { @Relation(entity = Stack.class, parentColumn = "localId", entityColumn = "boardId") public List<Stack> stacks; + @Ignore + public List<User> users; public User getOwner() { return owner; @@ -54,9 +57,17 @@ public class FullBoard implements IRemoteEntity { this.labels = labels; } + public List<User> getUsers() { + return users; + } + + public void setUsers(List<User> users) { + this.users = users; + } + @Ignore @Override - public IRemoteEntity getEntity() { + public Board getEntity() { return board; } @@ -102,6 +113,7 @@ public class FullBoard implements IRemoteEntity { return result; } + @NonNull @Override public String toString() { return "FullBoard{" + diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/full/FullCard.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/full/FullCard.java index 6f3913f45..611baf3df 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/model/full/FullCard.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/full/FullCard.java @@ -23,7 +23,7 @@ import it.niedermann.nextcloud.deck.model.ocs.comment.DeckComment; public class FullCard implements IRemoteEntity, DragAndDropModel { @Ignore - private transient boolean isAttachmentsSorted = false; + protected transient boolean isAttachmentsSorted = false; @Embedded public Card card; @@ -46,7 +46,6 @@ public class FullCard implements IRemoteEntity, DragAndDropModel { @Relation(entity = DeckComment.class, parentColumn = "localId", entityColumn = "objectId", projection = "localId") public List<Long> commentIDs; - public FullCard() { super(); } @@ -124,7 +123,7 @@ public class FullCard implements IRemoteEntity, DragAndDropModel { @Ignore @Override - public IRemoteEntity getEntity() { + public Card getEntity() { return card; } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/full/FullCardWithProjects.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/full/FullCardWithProjects.java new file mode 100644 index 000000000..cf43fc86b --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/full/FullCardWithProjects.java @@ -0,0 +1,82 @@ +package it.niedermann.nextcloud.deck.model.full; + +import androidx.annotation.NonNull; +import androidx.room.Junction; +import androidx.room.Relation; + +import java.util.ArrayList; +import java.util.List; + +import it.niedermann.nextcloud.deck.model.ocs.projects.JoinCardWithProject; +import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProject; +import it.niedermann.nextcloud.deck.model.ocs.projects.full.OcsProjectWithResources; + +public class FullCardWithProjects extends FullCard { + + + @NonNull + @Relation(entity = OcsProject.class, parentColumn = "localId", entityColumn = "localId", + associateBy = @Junction(value = JoinCardWithProject.class, parentColumn = "cardId", entityColumn = "projectId")) + + private List<OcsProjectWithResources> projects = new ArrayList<>(); + + public FullCardWithProjects() { + super(); + } + + public FullCardWithProjects(FullCardWithProjects fullCard) { + super(fullCard); + this.projects = copyList(fullCard.getProjects()); + } + + @NonNull + public List<OcsProjectWithResources> getProjects() { + return projects; + } + + public void setProjects(@NonNull List<OcsProjectWithResources> projects) { + this.projects = projects; + } + + @NonNull + @Override + public String toString() { + return "FullCard{" + + "card=" + card + + ", labels=" + labels + + ", assignedUsers=" + assignedUsers + + ", owner=" + owner + + ", attachments=" + attachments + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + FullCardWithProjects fullCard = (FullCardWithProjects) o; + + if (card != null ? !card.equals(fullCard.card) : fullCard.card != null) return false; + if (labels != null ? !labels.equals(fullCard.labels) : fullCard.labels != null) + return false; + if (assignedUsers != null ? !assignedUsers.equals(fullCard.assignedUsers) : fullCard.assignedUsers != null) + return false; + if (owner != null ? !owner.equals(fullCard.owner) : fullCard.owner != null) return false; + if (attachments != null ? !attachments.equals(fullCard.attachments) : fullCard.attachments != null) + return false; + return commentIDs != null ? commentIDs.equals(fullCard.commentIDs) : fullCard.commentIDs == null; + } + + @Override + public int hashCode() { + int result = (isAttachmentsSorted ? 1 : 0); + result = 31 * result + (card != null ? card.hashCode() : 0); + result = 31 * result + (labels != null ? labels.hashCode() : 0); + result = 31 * result + (assignedUsers != null ? assignedUsers.hashCode() : 0); + result = 31 * result + (owner != null ? owner.hashCode() : 0); + result = 31 * result + (attachments != null ? attachments.hashCode() : 0); + result = 31 * result + (commentIDs != null ? commentIDs.hashCode() : 0); + return result; + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/full/FullStack.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/full/FullStack.java index eddbafce4..1c9e94899 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/model/full/FullStack.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/full/FullStack.java @@ -14,7 +14,7 @@ public class FullStack implements IRemoteEntity { @Embedded public Stack stack; - @Relation(entity = Card.class, parentColumn = "localId", entityColumn = "stackId") + @Relation(entity = Card.class, parentColumn = "localId", entityColumn = "stackId") public List<Card> cards; @@ -36,7 +36,7 @@ public class FullStack implements IRemoteEntity { @Ignore @Override - public IRemoteEntity getEntity() { + public Stack getEntity() { return stack; } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/interfaces/AbstractJoinEntity.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/interfaces/AbstractJoinEntity.java index 50057dfe4..ce14ac6be 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/model/interfaces/AbstractJoinEntity.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/interfaces/AbstractJoinEntity.java @@ -1,21 +1,20 @@ package it.niedermann.nextcloud.deck.model.interfaces; -import androidx.annotation.NonNull; import androidx.room.Entity; import androidx.room.Ignore; + import it.niedermann.nextcloud.deck.model.enums.DBStatus; @Entity() public abstract class AbstractJoinEntity { - @NonNull protected int status = DBStatus.UP_TO_DATE.getId(); public int getStatus() { return status; } - public void setStatus(@NonNull int status) { + public void setStatus(int status) { this.status = status; } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/interfaces/AbstractRemoteEntity.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/interfaces/AbstractRemoteEntity.java index 328c642ae..c19e52970 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/model/interfaces/AbstractRemoteEntity.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/interfaces/AbstractRemoteEntity.java @@ -1,6 +1,5 @@ package it.niedermann.nextcloud.deck.model.interfaces; -import androidx.annotation.NonNull; import androidx.room.Entity; import androidx.room.ForeignKey; import androidx.room.Ignore; @@ -27,7 +26,7 @@ import it.niedermann.nextcloud.deck.model.enums.DBStatus; ) } ) -public abstract class AbstractRemoteEntity implements IRemoteEntity { +public abstract class AbstractRemoteEntity implements IRemoteEntity{ @PrimaryKey(autoGenerate = true) protected Long localId; @@ -35,7 +34,6 @@ public abstract class AbstractRemoteEntity implements IRemoteEntity { protected Long id; - @NonNull protected int status = DBStatus.UP_TO_DATE.getId(); protected Date lastModified; @@ -102,7 +100,7 @@ public abstract class AbstractRemoteEntity implements IRemoteEntity { @Override - public void setStatus(@NonNull int status) { + public void setStatus(int status) { this.status = status; } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/interfaces/IRemoteEntity.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/interfaces/IRemoteEntity.java index 30a478808..c878c132f 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/model/interfaces/IRemoteEntity.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/interfaces/IRemoteEntity.java @@ -1,7 +1,5 @@ package it.niedermann.nextcloud.deck.model.interfaces; -import androidx.annotation.NonNull; - import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -10,73 +8,62 @@ import it.niedermann.nextcloud.deck.model.enums.DBStatus; public interface IRemoteEntity { - default IRemoteEntity getEntity() {return this;} + default IRemoteEntity getEntity() { + return this; + } default Long getLocalId() { return getEntity().getLocalId(); } - default void setLocalId(Long localId) { getEntity().setLocalId(localId); } - default long getAccountId() { return getEntity().getAccountId(); } - default void setAccountId(long accountId) { getEntity().setAccountId(accountId); } - default Long getId() { return getEntity().getId(); } - default void setId(Long id) { getEntity().setId(id); } - default int getStatus() { return getEntity().getStatus(); } - - default void setStatus(@NonNull int status) { + default void setStatus(int status) { getEntity().setStatus(status); } - default Date getLastModified() { return getEntity().getLastModified(); } - default void setLastModified(Date lastModified) { getEntity().setLastModified(lastModified); } - default Date getLastModifiedLocal() { return getEntity().getLastModifiedLocal(); } - default void setLastModifiedLocal(Date lastModifiedLocal) { getEntity().setLastModifiedLocal(lastModifiedLocal); } - default DBStatus getStatusEnum() { return getEntity().getStatusEnum(); } - default void setStatusEnum(DBStatus status) { getEntity().setStatusEnum(status); } @@ -86,9 +73,7 @@ public interface IRemoteEntity { return null; } List<T> list = new ArrayList<>(listToCopy.size()); - for (T t : listToCopy) { - list.add(t); - } + list.addAll(listToCopy); return list; } } 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..1b5f0879d 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 @@ -12,12 +12,21 @@ import it.niedermann.nextcloud.deck.model.User; import it.niedermann.nextcloud.deck.model.enums.EDueType; public class FilterInformation implements Serializable { + + public enum EArchiveStatus{ + ALL, ARCHIVED, NON_ARCHIVED + } + @NonNull private EDueType dueType = EDueType.NO_FILTER; + private boolean noAssignedLabel = false; + private boolean noAssignedUser = false; @NonNull private List<User> users = new ArrayList<>(); @NonNull private List<Label> labels = new ArrayList<>(); + @NonNull + private EArchiveStatus archiveStatus = EArchiveStatus.NON_ARCHIVED; public FilterInformation() { // Default constructor @@ -26,8 +35,12 @@ public class FilterInformation implements Serializable { public FilterInformation(@Nullable FilterInformation filterInformation) { if (filterInformation != null) { this.dueType = filterInformation.getDueType(); + this.archiveStatus = filterInformation.getArchiveStatus(); this.users.addAll(filterInformation.getUsers()); this.labels.addAll(filterInformation.getLabels()); + this.noAssignedUser = filterInformation.isNoAssignedUser(); + this.noAssignedLabel = filterInformation.isNoAssignedLabel(); + this.archiveStatus = filterInformation.getArchiveStatus(); } } @@ -66,23 +79,55 @@ public class FilterInformation implements Serializable { users.remove(user); } + public boolean isNoAssignedUser() { + return noAssignedUser; + } + + public void setNoAssignedUser(boolean noAssignedUser) { + this.noAssignedUser = noAssignedUser; + } + + public boolean isNoAssignedLabel() { + return noAssignedLabel; + } + + public void setNoAssignedLabel(boolean noAssignedLabel) { + this.noAssignedLabel = noAssignedLabel; + } + + public void setArchiveStatus(@NonNull EArchiveStatus archiveStatus) { + this.archiveStatus = archiveStatus; + } + + @NonNull + public EArchiveStatus getArchiveStatus() { + return archiveStatus; + } + @NonNull @Override public String toString() { return "FilterInformation{" + "dueType=" + dueType + + ", noAssignedLabel=" + noAssignedLabel + + ", noAssignedUser=" + noAssignedUser + ", users=" + users + ", labels=" + labels + + ", archiveStatus=" + archiveStatus + '}'; } /** - * @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) { return false; } - return filterInformation.getDueType() != EDueType.NO_FILTER || filterInformation.getUsers().size() > 0 || filterInformation.getLabels().size() > 0; + return filterInformation.getDueType() != EDueType.NO_FILTER + || filterInformation.getUsers().size() > 0 + || filterInformation.getLabels().size() > 0 + || filterInformation.noAssignedUser + || filterInformation.noAssignedLabel; } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/Capabilities.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/Capabilities.java index 9ff4d7945..12e8692d4 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/Capabilities.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/Capabilities.java @@ -5,8 +5,8 @@ public class Capabilities { private Version deckVersion; private Version nextcloudVersion; - private String color = "#0082c9"; - private String textColor = "#ffffff"; + private int color = 0; + private int textColor = 0; private boolean maintenanceEnabled = false; public Capabilities() { @@ -28,19 +28,19 @@ public class Capabilities { this.nextcloudVersion = nextcloudVersion; } - public String getColor() { + public int getColor() { return color; } - public void setColor(String color) { + public void setColor(int color) { this.color = color; } - public String getTextColor() { + public int getTextColor() { return textColor; } - public void setTextColor(String textColor) { + public void setTextColor(int textColor) { this.textColor = textColor; } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/projects/JoinCardWithProject.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/projects/JoinCardWithProject.java new file mode 100644 index 000000000..615c7a2a2 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/projects/JoinCardWithProject.java @@ -0,0 +1,66 @@ +package it.niedermann.nextcloud.deck.model.ocs.projects; + +import androidx.annotation.NonNull; +import androidx.room.Entity; +import androidx.room.ForeignKey; +import androidx.room.Index; + +import it.niedermann.nextcloud.deck.model.Card; +import it.niedermann.nextcloud.deck.model.interfaces.AbstractJoinEntity; + +@Entity( + primaryKeys = {"projectId", "cardId"}, + indices = {@Index("cardId"), @Index("projectId")}, + foreignKeys = { + @ForeignKey(entity = OcsProject.class, + parentColumns = "localId", + childColumns = "projectId", + onDelete = ForeignKey.CASCADE + ), + @ForeignKey(entity = Card.class, + parentColumns = "localId", + childColumns = "cardId", + onDelete = ForeignKey.CASCADE + ) + }) +public class JoinCardWithProject extends AbstractJoinEntity { + @NonNull + private Long projectId; + @NonNull + private Long cardId; + + @NonNull + public Long getProjectId() { + return projectId; + } + + public void setProjectId(@NonNull Long projectId) { + this.projectId = projectId; + } + + public Long getCardId() { + return cardId; + } + + public void setCardId(Long cardId) { + this.cardId = cardId; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + JoinCardWithProject that = (JoinCardWithProject) o; + + if (!projectId.equals(that.projectId)) return false; + return cardId.equals(that.cardId); + } + + @Override + public int hashCode() { + int result = projectId.hashCode(); + result = 31 * result + cardId.hashCode(); + return result; + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/projects/OcsProject.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/projects/OcsProject.java new file mode 100644 index 000000000..49bc7296d --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/projects/OcsProject.java @@ -0,0 +1,45 @@ +package it.niedermann.nextcloud.deck.model.ocs.projects; + +import androidx.annotation.NonNull; +import androidx.room.Entity; +import androidx.room.Ignore; +import androidx.room.Index; + +import java.util.ArrayList; +import java.util.List; + +import it.niedermann.nextcloud.deck.model.interfaces.AbstractRemoteEntity; + +@Entity(inheritSuperIndices = true, + indices = { + @Index(value = "accountId", name = "index_project_accID"), + }, + foreignKeys = { + } +) +public class OcsProject extends AbstractRemoteEntity { + @NonNull + private String name; + + @Ignore + @NonNull + private ArrayList<OcsProjectResource> resources = new ArrayList<>(); + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @NonNull + public ArrayList<OcsProjectResource> getResources() { + return resources; + } + + public void setResources(@NonNull List<OcsProjectResource> resources) { + this.resources.clear(); + this.resources.addAll(resources); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/projects/OcsProjectList.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/projects/OcsProjectList.java new file mode 100644 index 000000000..af42b8432 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/projects/OcsProjectList.java @@ -0,0 +1,24 @@ +package it.niedermann.nextcloud.deck.model.ocs.projects; + +import java.util.ArrayList; +import java.util.List; + +public class OcsProjectList { + List<OcsProject> projects; + + public OcsProjectList() { + projects = new ArrayList<>(); + } + + public OcsProjectList(List<OcsProject> projects) { + this.projects = projects; + } + + public List<OcsProject> getProjects() { + return projects; + } + + public void add(OcsProject project) { + projects.add(project); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/projects/OcsProjectResource.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/projects/OcsProjectResource.java new file mode 100644 index 000000000..cd63ccde4 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/projects/OcsProjectResource.java @@ -0,0 +1,135 @@ +package it.niedermann.nextcloud.deck.model.ocs.projects; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.room.Entity; +import androidx.room.ForeignKey; +import androidx.room.Index; +import androidx.room.RoomWarnings; + +import java.io.Serializable; + +import it.niedermann.nextcloud.deck.model.interfaces.AbstractRemoteEntity; + +@SuppressWarnings(RoomWarnings.INDEX_FROM_PARENT_IS_DROPPED) +@Entity( + indices = { + @Index(value = "id", name = "index_OcsProjectResource_id"), + @Index(value = "lastModifiedLocal", name = "index_OcsProjectResource_lastModifiedLocal"), + @Index(value = {"accountId", "id", "idString", "projectId"}, name = "index_OcsProjectResource_accountId_id", unique = true), + @Index(value = "accountId", name = "index_projectResource_accID"), + @Index(value = "projectId", name = "index_projectResource_projectId"), + }, + foreignKeys = { + @ForeignKey( + entity = OcsProject.class, + parentColumns = "localId", + childColumns = "projectId", onDelete = ForeignKey.CASCADE + ) + } +) +public class OcsProjectResource extends AbstractRemoteEntity implements Serializable { + @Nullable + private String type; + @Nullable + private String name; + @Nullable + private String link; + @Nullable + private String path; + @Nullable + private String iconUrl; + @Nullable + private String mimetype; + @Nullable + private Boolean previewAvailable; + @Nullable + private String idString; + + + @NonNull + private Long projectId; + + public Long getProjectId() { + return projectId; + } + + public void setProjectId(Long projectId) { + this.projectId = projectId; + } + + @Nullable + public String getType() { + return type; + } + + public void setType(@Nullable String type) { + this.type = type; + } + + @Nullable + public String getName() { + return name; + } + + public void setName(@Nullable String name) { + this.name = name; + } + + /** + * Caution: the Link might be a full url or only the relative path! + * @return The link to the Resource + */ + @Nullable + public String getLink() { + return link; + } + + public void setLink(@Nullable String link) { + this.link = link; + } + + @Nullable + public String getIconUrl() { + return iconUrl; + } + + public void setIconUrl(@Nullable String iconUrl) { + this.iconUrl = iconUrl; + } + + @Nullable + public String getPath() { + return path; + } + + public void setPath(@Nullable String path) { + this.path = path; + } + + @Nullable + public String getMimetype() { + return mimetype; + } + + public void setMimetype(@Nullable String mimetype) { + this.mimetype = mimetype; + } + + public Boolean getPreviewAvailable() { + return Boolean.TRUE.equals(previewAvailable); + } + + public void setPreviewAvailable(@Nullable Boolean previewAvailable) { + this.previewAvailable = previewAvailable; + } + + @Nullable + public String getIdString() { + return idString; + } + + public void setIdString(@Nullable String idString) { + this.idString = idString; + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/projects/full/OcsProjectWithResources.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/projects/full/OcsProjectWithResources.java new file mode 100644 index 000000000..257cf6f60 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/projects/full/OcsProjectWithResources.java @@ -0,0 +1,50 @@ +package it.niedermann.nextcloud.deck.model.ocs.projects.full; + +import androidx.annotation.NonNull; +import androidx.room.Embedded; +import androidx.room.Relation; + +import java.util.List; + +import it.niedermann.nextcloud.deck.model.interfaces.IRemoteEntity; +import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProject; +import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProjectResource; + +public class OcsProjectWithResources implements IRemoteEntity { + @Embedded + public OcsProject project; + + + @Relation(entity = OcsProjectResource.class, parentColumn = "localId", entityColumn = "projectId") + public List<OcsProjectResource> resources; + + public OcsProject getProject() { + return project; + } + + public void setProject(OcsProject project) { + this.project = project; + } + + @NonNull + public List<OcsProjectResource> getResources() { + return resources; + } + + public void setResources(List<OcsProjectResource> resources) { + this.resources = resources; + } + + public String getName() { + return project.getName(); + } + + public void setName(String name) { + project.setName(name); + } + + @Override + public IRemoteEntity getEntity() { + return project; + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/user/GroupMemberUIDs.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/user/GroupMemberUIDs.java new file mode 100644 index 000000000..085ca786e --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/ocs/user/GroupMemberUIDs.java @@ -0,0 +1,27 @@ +package it.niedermann.nextcloud.deck.model.ocs.user; + +import java.util.ArrayList; +import java.util.List; + +public class GroupMemberUIDs { + private List<String> uids = new ArrayList<>(); + + public List<String> getUids() { + return uids; + } + + public void setUids(List<String> uids) { + this.uids = uids; + } + + @Override + public String toString() { + return "GroupMemberUIDs{" + + "uids=" + uids + + '}'; + } + + public void add(String uid) { + uids.add(uid); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/relations/UserInBoard.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/relations/UserInBoard.java new file mode 100644 index 000000000..1f167bad1 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/relations/UserInBoard.java @@ -0,0 +1,71 @@ +package it.niedermann.nextcloud.deck.model.relations; + +import androidx.annotation.NonNull; +import androidx.room.Entity; +import androidx.room.ForeignKey; +import androidx.room.Index; + +import it.niedermann.nextcloud.deck.model.Board; +import it.niedermann.nextcloud.deck.model.User; + +@Entity( + primaryKeys = {"userId", "boardId"}, + indices = {@Index("userId"), @Index("boardId"), @Index(name = "unique_idx_user_board", value = {"userId","boardId"}, unique = true)}, + foreignKeys = { + @ForeignKey(entity = User.class, + parentColumns = "localId", + childColumns = "userId", onDelete = ForeignKey.CASCADE), + @ForeignKey(entity = Board.class, + parentColumns = "localId", + childColumns = "boardId", onDelete = ForeignKey.CASCADE) + }) +public class UserInBoard { + @NonNull + private Long userId; + @NonNull + private Long boardId; + + @NonNull + public Long getUserId() { + return userId; + } + + public void setUserId(@NonNull Long userId) { + this.userId = userId; + } + + @NonNull + public Long getBoardId() { + return boardId; + } + + public void setBoardId(@NonNull Long boardId) { + this.boardId = boardId; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + UserInBoard that = (UserInBoard) o; + + if (!userId.equals(that.userId)) return false; + return boardId.equals(that.boardId); + } + + @Override + public int hashCode() { + int result = userId.hashCode(); + result = 31 * result + boardId.hashCode(); + return result; + } + + @Override + public String toString() { + return "UserInGroup{" + + "userId=" + userId + + ", boardId=" + boardId + + '}'; + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/relations/UserInGroup.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/relations/UserInGroup.java new file mode 100644 index 000000000..48e16cce5 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/relations/UserInGroup.java @@ -0,0 +1,70 @@ +package it.niedermann.nextcloud.deck.model.relations; + +import androidx.annotation.NonNull; +import androidx.room.Entity; +import androidx.room.ForeignKey; +import androidx.room.Index; + +import it.niedermann.nextcloud.deck.model.User; + +@Entity( + primaryKeys = {"groupId", "memberId"}, + indices = {@Index("groupId"), @Index("memberId"), @Index(name = "unique_idx_group_member", value = {"groupId","memberId"}, unique = true)}, + foreignKeys = { + @ForeignKey(entity = User.class, + parentColumns = "localId", + childColumns = "groupId", onDelete = ForeignKey.CASCADE), + @ForeignKey(entity = User.class, + parentColumns = "localId", + childColumns = "memberId", onDelete = ForeignKey.CASCADE) + }) +public class UserInGroup { + @NonNull + private Long groupId; + @NonNull + private Long memberId; + + @NonNull + public Long getGroupId() { + return groupId; + } + + public void setGroupId(@NonNull Long groupId) { + this.groupId = groupId; + } + + @NonNull + public Long getMemberId() { + return memberId; + } + + public void setMemberId(@NonNull Long memberId) { + this.memberId = memberId; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + UserInGroup that = (UserInGroup) o; + + if (!groupId.equals(that.groupId)) return false; + return memberId.equals(that.memberId); + } + + @Override + public int hashCode() { + int result = groupId.hashCode(); + result = 31 * result + memberId.hashCode(); + return result; + } + + @Override + public String toString() { + return "UserInGroup{" + + "groupId=" + groupId + + ", memberId=" + memberId + + '}'; + } +} 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 0f9845dee..65873c357 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 @@ -1,31 +1,42 @@ package it.niedermann.nextcloud.deck.persistence.sync; +import android.annotation.SuppressLint; import android.content.Context; import android.database.sqlite.SQLiteConstraintException; import androidx.annotation.AnyThread; +import androidx.annotation.ColorInt; +import androidx.annotation.MainThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.Size; import androidx.annotation.WorkerThread; import androidx.core.util.Pair; import androidx.lifecycle.LiveData; import androidx.lifecycle.MediatorLiveData; import androidx.lifecycle.MutableLiveData; +import com.nextcloud.android.sso.api.ParsedResponse; import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException; import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.Date; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.NoSuchElementException; +import java.util.concurrent.ConcurrentHashMap; 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; import it.niedermann.nextcloud.deck.api.IResponseCallback; import it.niedermann.nextcloud.deck.api.LastSyncUtil; +import it.niedermann.nextcloud.deck.exceptions.DeckException; import it.niedermann.nextcloud.deck.exceptions.OfflineException; import it.niedermann.nextcloud.deck.model.AccessControl; import it.niedermann.nextcloud.deck.model.Account; @@ -36,9 +47,11 @@ import it.niedermann.nextcloud.deck.model.JoinCardWithUser; import it.niedermann.nextcloud.deck.model.Label; import it.niedermann.nextcloud.deck.model.Stack; import it.niedermann.nextcloud.deck.model.User; +import it.niedermann.nextcloud.deck.model.appwidgets.StackWidgetModel; import it.niedermann.nextcloud.deck.model.enums.DBStatus; import it.niedermann.nextcloud.deck.model.full.FullBoard; import it.niedermann.nextcloud.deck.model.full.FullCard; +import it.niedermann.nextcloud.deck.model.full.FullCardWithProjects; import it.niedermann.nextcloud.deck.model.full.FullSingleCardWidgetModel; import it.niedermann.nextcloud.deck.model.full.FullStack; import it.niedermann.nextcloud.deck.model.internal.FilterInformation; @@ -46,13 +59,11 @@ 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.model.ocs.projects.OcsProjectResource; 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; @@ -66,9 +77,11 @@ 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_NOT_MODIFIED; import static java.net.HttpURLConnection.HTTP_UNAVAILABLE; @SuppressWarnings("WeakerAccess") @@ -81,6 +94,8 @@ public class SyncManager { @NonNull private ServerAdapter serverAdapter; + private static final Map<Long, List<IResponseCallback<Boolean>>> RUNNING_SYNCS = new ConcurrentHashMap<>(); + @AnyThread public SyncManager(@NonNull Context context) { this(context, null); @@ -100,33 +115,6 @@ public class SyncManager { } @AnyThread - public MutableLiveData<FullCard> synchronizeCardByRemoteId(long cardRemoteId, @NonNull Account account) { - MutableLiveData<FullCard> liveData = new MutableLiveData<>(); - doAsync(() -> { - Long accountId = account.getId(); - Card card = dataBaseAdapter.getCardByRemoteIdDirectly(accountId, cardRemoteId); - FullStack stack = dataBaseAdapter.getFullStackByLocalIdDirectly(card.getStackId()); - // only sync this one card. - stack.setCards(Collections.singletonList(card)); - Board board = dataBaseAdapter.getBoardByLocalIdDirectly(stack.getStack().getBoardId()); - new SyncHelper(serverAdapter, dataBaseAdapter, new Date()).setResponseCallback(new IResponseCallback<Boolean>(account) { - @Override - public void onResponse(Boolean response) { - FullCard fullCard = dataBaseAdapter.getFullCardByLocalIdDirectly(accountId, card.getLocalId()); - liveData.postValue(fullCard); - } - - @Override - public void onError(Throwable throwable) { - liveData.postValue(null); - } - }).doSyncFor(new CardDataProvider(null, board, stack)); - }); - return liveData; - } - - // TODO if the card does not exist yet, try to synchronize it first, instead of directly returning null. If sync failed, return null. - @AnyThread public LiveData<Long> getLocalBoardIdByCardRemoteIdAndAccount(long cardRemoteId, @NonNull Account account) { return dataBaseAdapter.getLocalBoardIdByCardRemoteIdAndAccountId(cardRemoteId, account.getId()); } @@ -166,76 +154,161 @@ public class SyncManager { @AnyThread public void synchronize(@NonNull IResponseCallback<Boolean> responseCallback) { - if(responseCallback.getAccount() == null) { + synchronize(Collections.singletonList(responseCallback)); + } + + @AnyThread + public void synchronizeBoard(@NonNull IResponseCallback<Boolean> responseCallback, long localBoadId) { + doAsync(() -> { + FullBoard board = dataBaseAdapter.getFullBoardByLocalIdDirectly(responseCallback.getAccount().getId(), localBoadId); + try { + new SyncHelper(serverAdapter, dataBaseAdapter, null).setResponseCallback(responseCallback).doSyncFor(new StackDataProvider(null, board)); + } catch (OfflineException e) { + responseCallback.onError(e); + } + }); + } + + @AnyThread + public void synchronizeCard(@NonNull IResponseCallback<Boolean> responseCallback, Card card) { + doAsync(() -> { + FullStack stack = dataBaseAdapter.getFullStackByLocalIdDirectly(card.getStackId()); + Board board = dataBaseAdapter.getBoardByLocalIdDirectly(stack.getStack().getBoardId()); + try { + new SyncHelper(serverAdapter, dataBaseAdapter, null).setResponseCallback(responseCallback).doSyncFor(new CardDataProvider(null, board, stack)); + } catch (OfflineException e) { + responseCallback.onError(e); + } + }); + } + + private void synchronize(@NonNull @Size(min = 1) List<IResponseCallback<Boolean>> responseCallbacks) { + if (responseCallbacks == null || responseCallbacks.size() < 1) { + return; + } + IResponseCallback<Boolean> responseCallback = responseCallbacks.get(0); + Account callbackAccount = responseCallback.getAccount(); + if (callbackAccount == null) { throw new IllegalArgumentException(Account.class.getSimpleName() + " object in given " + IResponseCallback.class.getSimpleName() + " must not be null."); } - if(responseCallback.getAccount().getId() == null) { + Long callbackAccountId = callbackAccount.getId(); + if (callbackAccountId == null) { throw new IllegalArgumentException(Account.class.getSimpleName() + " object in given " + IResponseCallback.class.getSimpleName() + " must contain a valid id, but given id was null."); } - doAsync(() -> refreshCapabilities(new IResponseCallback<Capabilities>(responseCallback.getAccount()) { - @Override - public void onResponse(Capabilities response) { - if (!response.isMaintenanceEnabled()) { - if (response.getDeckVersion().isSupported(appContext)) { - long accountId = responseCallback.getAccount().getId(); - Date lastSyncDate = LastSyncUtil.getLastSyncDate(responseCallback.getAccount().getId()); - Date now = DateUtil.nowInGMT(); - - final SyncHelper syncHelper = new SyncHelper(serverAdapter, dataBaseAdapter, lastSyncDate); + List<IResponseCallback<Boolean>> queuedCallbacks = RUNNING_SYNCS.get(callbackAccountId); + if (queuedCallbacks != null) { + queuedCallbacks.addAll(responseCallbacks); + return; + } else { + RUNNING_SYNCS.put(callbackAccountId, new ArrayList<>(responseCallbacks)); + } + doAsync(() -> { + List<IResponseCallback<Boolean>> existingQueue = RUNNING_SYNCS.get(callbackAccountId); + List<IResponseCallback<Boolean>> callbacksQueueForSync = existingQueue == null ? new ArrayList<>() : new ArrayList<>(existingQueue); + refreshCapabilities(new IResponseCallback<Capabilities>(responseCallback.getAccount()) { + @Override + public void onResponse(Capabilities response) { + if (response != null && !response.isMaintenanceEnabled()) { + if (response.getDeckVersion().isSupported(appContext)) { + long accountId = callbackAccountId; + Date lastSyncDate = LastSyncUtil.getLastSyncDate(callbackAccountId); + Date now = DateUtil.nowInGMT(); - IResponseCallback<Boolean> callback = new IResponseCallback<Boolean>(responseCallback.getAccount()) { - @Override - public void onResponse(Boolean response) { - syncHelper.setResponseCallback(new IResponseCallback<Boolean>(account) { - @Override - public void onResponse(Boolean response) { - // TODO deactivate for dev - LastSyncUtil.setLastSyncDate(accountId, now); - responseCallback.onResponse(response); - } + final SyncHelper syncHelper = new SyncHelper(serverAdapter, dataBaseAdapter, lastSyncDate); - @Override - public void onError(Throwable throwable) { - super.onError(throwable); - responseCallback.onError(throwable); - } - }); - doAsync(() -> { - try { - syncHelper.doUpSyncFor(new BoardDataProvider()); - } catch (Throwable e) { - DeckLog.logError(e); - responseCallback.onError(e); - } - }); + IResponseCallback<Boolean> callback = new IResponseCallback<Boolean>(callbackAccount) { + @Override + public void onResponse(Boolean response) { + syncHelper.setResponseCallback(new IResponseCallback<Boolean>(account) { + @Override + public void onResponse(Boolean response) { + // TODO deactivate for dev + LastSyncUtil.setLastSyncDate(accountId, now); + respondCallbacksAfterSync(callbacksQueueForSync, response, null); + } + + @Override + public void onError(Throwable throwable) { + super.onError(throwable); + respondCallbacksAfterSync(callbacksQueueForSync, null, throwable); + } + }); + doAsync(() -> { + try { + syncHelper.doUpSyncFor(new BoardDataProvider()); + } catch (Throwable e) { + DeckLog.logError(e); + respondCallbacksAfterSync(callbacksQueueForSync, null, e); + } + }); - } + } - @Override - public void onError(Throwable throwable) { - super.onError(throwable); - responseCallback.onError(throwable); - } - }; + @Override + public void onError(Throwable throwable) { + super.onError(throwable); + respondCallbacksAfterSync(callbacksQueueForSync, null, throwable); + } + }; - syncHelper.setResponseCallback(callback); + syncHelper.setResponseCallback(callback); - try { - syncHelper.doSyncFor(new BoardDataProvider()); - } catch (Throwable e) { - DeckLog.logError(e); - responseCallback.onError(e); + try { + syncHelper.doSyncFor(new BoardDataProvider()); + } catch (Throwable e) { + DeckLog.logError(e); + respondCallbacksAfterSync(callbacksQueueForSync, null, e); + } + } else { + respondCallbacksAfterSync(callbacksQueueForSync, Boolean.FALSE, null); + DeckLog.warn("No sync. Server version not supported: " + response.getDeckVersion().getOriginalVersion()); } } else { - responseCallback.onResponse(false); - DeckLog.warn("No sync. Server version not supported: " + response.getDeckVersion().getOriginalVersion()); + respondCallbacksAfterSync(callbacksQueueForSync, Boolean.FALSE, null); + if (response != null) { + DeckLog.warn("No sync. Status maintenance mode: " + response.isMaintenanceEnabled()); + } } - } else { - responseCallback.onResponse(false); - DeckLog.warn("No sync. Status maintenance mode: " + response.isMaintenanceEnabled()); } + }); + }); + } + + private void respondCallbacksAfterSync(List<IResponseCallback<Boolean>> callbacksQueueForSync, Boolean response, Throwable throwable) { + if (callbacksQueueForSync == null || callbacksQueueForSync.isEmpty()) { + return; + } + // notify done callbacks + DeckLog.info("SyncQueue: responding sync for " + callbacksQueueForSync.size() + " queued callbacks!"); + List<IResponseCallback<Boolean>> callbacksQueue = new ArrayList<>(callbacksQueueForSync); + if (throwable == null) { + //success: + for (IResponseCallback<Boolean> callback : callbacksQueue) { + if (callback != null) callback.onResponse(response); + } + } else { + // failure: + for (IResponseCallback<Boolean> callback : callbacksQueue) { + if (callback != null) callback.onError(throwable); } - })); + } + // remove done callbacks from queue + IResponseCallback<Boolean> firstCallbackOfAccount = callbacksQueue.iterator().next(); + List<IResponseCallback<Boolean>> queuedCallbacks = RUNNING_SYNCS.get(firstCallbackOfAccount.getAccount().getId()); + if (queuedCallbacks == null) { + return; + } + for (IResponseCallback<Boolean> callback : callbacksQueue) { + queuedCallbacks.remove(callback); + } + // cleanup if done, or proceed if not + if (queuedCallbacks.isEmpty()) { + RUNNING_SYNCS.remove(firstCallbackOfAccount.getAccount().getId()); + } else { + DeckLog.info("SyncQueue: starting sync for " + queuedCallbacks.size() + " queued callbacks!"); + RUNNING_SYNCS.remove(firstCallbackOfAccount.getAccount().getId()); + synchronize(queuedCallbacks); + } } // @@ -322,7 +395,7 @@ public class SyncManager { * - located at the given {@param host} * - and have the permission to read the board with the given {@param boardRemoteId} (aka the {@link Board} is shared with this {@link User}). */ - @AnyThread + @MainThread public LiveData<List<Account>> readAccountsForHostWithReadAccessToBoard(String host, long boardRemoteId) { MediatorLiveData<List<Account>> liveData = new MediatorLiveData<>(); liveData.addSource(dataBaseAdapter.readAccountsForHostWithReadAccessToBoard(host, boardRemoteId), accounts -> { @@ -335,7 +408,7 @@ public class SyncManager { public void onResponse(Boolean response) { liveData.postValue(dataBaseAdapter.readAccountsForHostWithReadAccessToBoardDirectly(host, boardRemoteId)); } - }).doSyncFor(new BoardWitAclDownSyncDataProvider()); + }).doSyncFor(new BoardWithAclDownSyncDataProvider()); } }); }); @@ -347,15 +420,17 @@ public class SyncManager { public void refreshCapabilities(@NonNull IResponseCallback<Capabilities> callback) { doAsync(() -> { try { - serverAdapter.getCapabilities(new IResponseCallback<Capabilities>(callback.getAccount()) { + Account accountForEtag = dataBaseAdapter.getAccountByIdDirectly(callback.getAccount().getId()); + serverAdapter.getCapabilities(accountForEtag.getEtag(), new IResponseCallback<ParsedResponse<Capabilities>>(callback.getAccount()) { @Override - public void onResponse(Capabilities response) { + public void onResponse(ParsedResponse<Capabilities> response) { Account acc = dataBaseAdapter.getAccountByIdDirectly(account.getId()); - acc.applyCapabilities(response); + acc.applyCapabilities(response.getResponse(), response.getHeaders().get("ETag")); dataBaseAdapter.updateAccount(acc); - callback.onResponse(response); + callback.onResponse(response.getResponse()); } + @SuppressLint("MissingSuperCall") @Override public void onError(Throwable throwable) { if (throwable instanceof NextcloudHttpRequestFailedException) { @@ -363,13 +438,28 @@ public class SyncManager { if (requestFailedException.getStatusCode() == HTTP_UNAVAILABLE && requestFailedException.getCause() != null) { String errorString = requestFailedException.getCause().getMessage(); Capabilities capabilities = GsonConfig.getGson().fromJson(errorString, Capabilities.class); + DeckLog.verbose("HTTP Status " + HTTP_UNAVAILABLE + ": This server seems to be in maintenance mode."); if (capabilities.isMaintenanceEnabled()) { - doAsync(() -> { - onResponse(capabilities); - }); + doAsync(() -> onResponse(ParsedResponse.of(capabilities))); } else { onError(throwable); } + } else if (requestFailedException.getStatusCode() == HTTP_NOT_MODIFIED) { + DeckLog.verbose("HTTP Status " + HTTP_NOT_MODIFIED + ": There haven't been any changes on the server side for this request."); + //could be after maintenance. so we have to at least revert the maintenance flag + doAsync(() -> { + Account acc = dataBaseAdapter.getAccountByIdDirectly(account.getId()); + if (acc.isMaintenanceEnabled()) { + acc.setMaintenanceEnabled(false); + dataBaseAdapter.updateAccount(acc); + } + Capabilities capabilities = new Capabilities(); + capabilities.setMaintenanceEnabled(false); + capabilities.setDeckVersion(acc.getServerDeckVersionAsObject()); + capabilities.setTextColor(acc.getTextColor()); + capabilities.setColor(acc.getColor()); + callback.onResponse(capabilities); + }); } } else { callback.onError(throwable); @@ -384,6 +474,25 @@ public class SyncManager { /** * @param accountId ID of the account + * @return all {@link Board}s no matter if {@link Board#archived} or not. + */ + @SuppressWarnings("JavadocReference") + @AnyThread + public LiveData<List<Board>> getBoards(long accountId) { + return dataBaseAdapter.getBoards(accountId); + } + + /** + * @param localProjectId LocalId of the OcsProject + * @return all {@link OcsProjectResource}s of the Project + */ + @AnyThread + public LiveData<List<OcsProjectResource>> getResourcesForProject(long localProjectId) { + return dataBaseAdapter.getResourcesByLocalProjectId(localProjectId); + } + + /** + * @param accountId ID of the account * @param archived Decides whether only archived or not-archived boards for the specified account will be returned * @return all archived or non-archived <code>Board</code>s depending on <code>archived</code> parameter */ @@ -448,58 +557,144 @@ public class SyncManager { /** * Creates a new {@link Board} and adds the same {@link Label} and {@link Stack} as in the origin {@link Board}. * Owner of the target {@link Board} will be the {@link User} with the {@link Account} of {@param targetAccountId}. - * Does <strong>not</strong> clone any {@link Card} or {@link AccessControl} from the origin {@link Board}. + * + * @param cloneCards determines whether or not the cards in this {@link Board} shall be cloned or not + * Does <strong>not</strong> clone any {@link Card} or {@link AccessControl} from the origin {@link Board}. + * <p> + * TODO implement https://github.com/stefan-niedermann/nextcloud-deck/issues/608 */ @AnyThread - public WrappedLiveData<FullBoard> cloneBoard(long originAccountId, long originBoardLocalId, long targetAccountId, String targetBoardTitle, String targetBoardColor) { - WrappedLiveData<FullBoard> liveData = new WrappedLiveData<>(); + public WrappedLiveData<FullBoard> cloneBoard(long originAccountId, long originBoardLocalId, long targetAccountId, @ColorInt int targetBoardColor, boolean cloneCards) { + final WrappedLiveData<FullBoard> liveData = new WrappedLiveData<>(); doAsync(() -> { Account originAccount = dataBaseAdapter.getAccountByIdDirectly(originAccountId); User newOwner = dataBaseAdapter.getUserByUidDirectly(originAccountId, originAccount.getUserName()); + if (newOwner == null) { + liveData.postError(new DeckException(DeckException.Hint.UNKNOWN_ACCOUNT_USER_ID, "User with Account-UID \"" + originAccount.getUserName() + "\" not found.")); + return; + } 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.getBoard().setColor(targetBoardColor); - originalBoard.getBoard().setOwnerId(newOwner.getId()); - originalBoard.setStatusEnum(DBStatus.LOCAL_EDITED); - originalBoard.setOwner(newOwner); originalBoard.setId(null); originalBoard.setLocalId(null); + originalBoard.getBoard().setTitle(newBoardTitle); + originalBoard.getBoard().setColor(String.format("%06X", 0xFFFFFF & targetBoardColor)); + originalBoard.getBoard().setOwnerId(newOwner.getLocalId()); + originalBoard.setStatusEnum(DBStatus.LOCAL_EDITED); + originalBoard.setOwner(newOwner); long newBoardId = dataBaseAdapter.createBoardDirectly(originAccountId, originalBoard.getBoard()); originalBoard.setLocalId(newBoardId); - for (Stack stack : originalBoard.getStacks()) { - stack.setLocalId(null); - stack.setId(null); - stack.setStatusEnum(DBStatus.LOCAL_EDITED); - stack.setAccountId(targetAccountId); - stack.setBoardId(newBoardId); - dataBaseAdapter.createStack(targetAccountId, stack); + boolean isSameAccount = targetAccountId == originAccountId; + + if (isSameAccount) { + List<AccessControl> aclList = originalBoard.getParticipants(); + for (AccessControl acl : aclList) { + acl.setLocalId(null); + acl.setId(null); + acl.setBoardId(newBoardId); + dataBaseAdapter.createAccessControl(targetAccountId, acl); + } } + + Map<Long, Long> oldToNewLabelIdsDictionary = new HashMap<>(); + for (Label label : originalBoard.getLabels()) { + Long oldLocalId = label.getLocalId(); label.setLocalId(null); label.setId(null); label.setAccountId(targetAccountId); label.setStatusEnum(DBStatus.LOCAL_EDITED); label.setBoardId(newBoardId); - dataBaseAdapter.createLabel(targetAccountId, label); + long newLocalId = dataBaseAdapter.createLabelDirectly(targetAccountId, label); + oldToNewLabelIdsDictionary.put(oldLocalId, newLocalId); } - 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); + List<Stack> oldStacks = originalBoard.getStacks(); + for (Stack stack : oldStacks) { + Long oldStackId = stack.getLocalId(); + stack.setLocalId(null); + stack.setId(null); + stack.setStatusEnum(DBStatus.LOCAL_EDITED); + stack.setAccountId(targetAccountId); + stack.setBoardId(newBoardId); + long createdStackId = dataBaseAdapter.createStack(targetAccountId, stack); + if (cloneCards) { + List<FullCard> oldCards = dataBaseAdapter.getFullCardsForStackDirectly(originAccountId, oldStackId, null); + for (FullCard oldCard : oldCards) { + Card newCard = oldCard.getCard(); + newCard.setId(null); + newCard.setUserId(newOwner.getLocalId()); + newCard.setLocalId(null); + newCard.setStackId(createdStackId); + newCard.setAccountId(targetAccountId); + newCard.setStatusEnum(DBStatus.LOCAL_EDITED); + long createdCardId = dataBaseAdapter.createCardDirectly(targetAccountId, newCard); + if (oldCard.getLabels() != null) { + for (Label oldLabel : oldCard.getLabels()) { + Long newLabelId = oldToNewLabelIdsDictionary.get(oldLabel.getLocalId()); + if (newLabelId != null) { + dataBaseAdapter.createJoinCardWithLabel(newLabelId, createdCardId, DBStatus.LOCAL_EDITED); + } else + DeckLog.error("ID of created Label is null! Skipping assignment of \"" + oldLabel.getTitle() + "\"..."); + } + } + if (isSameAccount && oldCard.getAssignedUsers() != null) { + for (User assignedUser : oldCard.getAssignedUsers()) { + dataBaseAdapter.createJoinCardWithUser(assignedUser.getLocalId(), createdCardId, DBStatus.LOCAL_EDITED); + } } - }).doSyncFor(new BoardDataProvider()); + } + } + } + // dont trigger concurrent syncs! + List<IResponseCallback<Boolean>> queuedSync = RUNNING_SYNCS.get(targetAccountId); + if ((queuedSync == null || queuedSync.isEmpty()) && 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(dataBaseAdapter.getFullBoardByLocalIdDirectly(targetAccountId, newBoardId))); + } else { + liveData.postValue(dataBaseAdapter.getFullBoardByLocalIdDirectly(targetAccountId, newBoardId)); + } }); return liveData; } @@ -598,6 +793,7 @@ public class SyncManager { liveData.postValue(response); } + @SuppressLint("MissingSuperCall") @Override public void onError(Throwable throwable) { liveData.postError(throwable); @@ -607,8 +803,8 @@ public class SyncManager { return liveData; } - public LiveData<List<FullStack>> getStacksForBoard(long accountId, long localBoardId) { - return dataBaseAdapter.getFullStacksForBoard(accountId, localBoardId); + public LiveData<List<Stack>> getStacksForBoard(long accountId, long localBoardId) { + return dataBaseAdapter.getStacksForBoard(accountId, localBoardId); } public LiveData<FullStack> getStack(long accountId, long localStackId) { @@ -625,8 +821,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; @@ -661,6 +856,7 @@ public class SyncManager { liveData.postValue(response); } + @SuppressLint("MissingSuperCall") @Override public void onError(Throwable throwable) { liveData.postError(throwable); @@ -685,6 +881,7 @@ public class SyncManager { liveData.postValue(response); } + @SuppressLint("MissingSuperCall") @Override public void onError(Throwable throwable) { liveData.postError(throwable); @@ -699,13 +896,14 @@ public class SyncManager { } @AnyThread - public WrappedLiveData<FullStack> createStack(long accountId, @NonNull Stack stack) { + public WrappedLiveData<FullStack> createStack(long accountId, @NonNull String title, long boardLocalId) { WrappedLiveData<FullStack> liveData = new WrappedLiveData<>(); doAsync(() -> { + Stack stack = new Stack(title, boardLocalId); Account account = dataBaseAdapter.getAccountByIdDirectly(accountId); FullBoard board = dataBaseAdapter.getFullBoardByLocalIdDirectly(accountId, stack.getBoardId()); FullStack fullStack = new FullStack(); - // TODO set stack order to (highest stack-order from board) + 1 and remove logic from caller + stack.setOrder(dataBaseAdapter.getHighestStackOrderInBoard(stack.getBoardId()) + 1); stack.setAccountId(accountId); stack.setBoardId(board.getLocalId()); fullStack.setStack(stack); @@ -728,15 +926,16 @@ public class SyncManager { } @AnyThread - public WrappedLiveData<FullStack> updateStack(@NonNull FullStack stack) { + public WrappedLiveData<FullStack> updateStackTitle(long localStackId, @NonNull String newTitle) { WrappedLiveData<FullStack> liveData = new WrappedLiveData<>(); doAsync(() -> { + FullStack stack = dataBaseAdapter.getFullStackByLocalIdDirectly(localStackId); + FullBoard fullBoard = dataBaseAdapter.getFullBoardByLocalIdDirectly(stack.getAccountId(), stack.getStack().getBoardId()); Account account = dataBaseAdapter.getAccountByIdDirectly(stack.getAccountId()); - FullBoard board = dataBaseAdapter.getFullBoardByLocalIdDirectly(stack.getAccountId(), stack.getStack().getBoardId()); - updateStack(account, board, stack, liveData); + stack.getStack().setTitle(newTitle); + updateStack(account, fullBoard, stack, liveData); }); return liveData; - } @AnyThread @@ -750,6 +949,7 @@ public class SyncManager { } } + @SuppressLint("MissingSuperCall") @Override public void onError(Throwable throwable) { if (liveData != null) { @@ -787,8 +987,8 @@ public class SyncManager { }); } - public LiveData<FullCard> getCardByLocalId(long accountId, long cardLocalId) { - return dataBaseAdapter.getCardByLocalId(accountId, cardLocalId); + public LiveData<FullCardWithProjects> getFullCardWithProjectsByLocalId(long accountId, long cardLocalId) { + return dataBaseAdapter.getCardWithProjectsByLocalId(accountId, cardLocalId); } public LiveData<List<FullCard>> getFullCardsForStack(long accountId, long localStackId, @Nullable FilterInformation filter) { @@ -849,7 +1049,7 @@ public class SyncManager { card.getCard().setAccountId(accountId); card.getCard().setStatusEnum(DBStatus.LOCAL_EDITED); card.getCard().setOrder(dataBaseAdapter.getHighestCardOrderInStack(localStackId) + 1); - long localCardId = dataBaseAdapter.createCard(accountId, card.getCard()); + long localCardId = dataBaseAdapter.createCardDirectly(accountId, card.getCard()); card.getCard().setLocalId(localCardId); List<User> assignedUsers = card.getAssignedUsers(); @@ -891,7 +1091,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()); @@ -909,12 +1109,12 @@ public class SyncManager { FullStack stack = dataBaseAdapter.getFullStackByLocalIdDirectly(card.getCard().getStackId()); Board board = dataBaseAdapter.getBoardByLocalIdDirectly(stack.getStack().getBoardId()); card.getCard().setArchived(true); - updateCardForArchive(account, stack, board, card, getCallbackToLiveDataConverter(account, liveData)); + updateCardForArchive(stack, board, card, getCallbackToLiveDataConverter(account, liveData)); }); return liveData; } - private void updateCardForArchive(Account account, FullStack stack, Board board, FullCard card, @NonNull IResponseCallback<FullCard> callback) { + private void updateCardForArchive(FullStack stack, Board board, FullCard card, @NonNull IResponseCallback<FullCard> callback) { new DataPropagationHelper(serverAdapter, dataBaseAdapter).updateEntity(new CardDataProvider(null, board, stack), card, callback); } @@ -926,29 +1126,34 @@ public class SyncManager { FullStack stack = dataBaseAdapter.getFullStackByLocalIdDirectly(card.getCard().getStackId()); Board board = dataBaseAdapter.getBoardByLocalIdDirectly(stack.getStack().getBoardId()); card.getCard().setArchived(false); - updateCardForArchive(account, stack, board, card, getCallbackToLiveDataConverter(account, liveData)); + updateCardForArchive(stack, board, card, getCallbackToLiveDataConverter(account, liveData)); }); return liveData; } @AnyThread - public WrappedLiveData<Void> archiveCardsInStack(long accountId, long stackLocalId) { + public WrappedLiveData<Void> archiveCardsInStack(long accountId, long stackLocalId, @NonNull FilterInformation filterInformation) { WrappedLiveData<Void> liveData = new WrappedLiveData<>(); doAsync(() -> { Account account = dataBaseAdapter.getAccountByIdDirectly(accountId); FullStack stack = dataBaseAdapter.getFullStackByLocalIdDirectly(stackLocalId); Board board = dataBaseAdapter.getBoardByLocalIdDirectly(stack.getStack().getBoardId()); - List<FullCard> cards = dataBaseAdapter.getFullCardsForStackDirectly(accountId, stackLocalId); + List<FullCard> cards = dataBaseAdapter.getFullCardsForStackDirectly(accountId, stackLocalId, filterInformation); if (cards.size() > 0) { CountDownLatch latch = new CountDownLatch(cards.size()); for (FullCard card : cards) { + if (card.getCard().isArchived()){ + latch.countDown(); + continue; + } card.getCard().setArchived(true); - updateCardForArchive(account, stack, board, card, new IResponseCallback<FullCard>(account) { + updateCardForArchive(stack, board, card, new IResponseCallback<FullCard>(account) { @Override public void onResponse(FullCard response) { latch.countDown(); } + @SuppressLint("MissingSuperCall") @Override public void onError(Throwable throwable) { latch.countDown(); @@ -970,21 +1175,35 @@ public class SyncManager { } @AnyThread - public void archiveBoard(@NonNull Board board) { + public WrappedLiveData<FullBoard> archiveBoard(@NonNull Board board) { + WrappedLiveData<FullBoard> liveData = new WrappedLiveData<>(); doAsync(() -> { - FullBoard b = dataBaseAdapter.getFullBoardByLocalIdDirectly(board.getAccountId(), board.getLocalId()); - b.getBoard().setArchived(true); - updateBoard(b); + try { + FullBoard b = dataBaseAdapter.getFullBoardByLocalIdDirectly(board.getAccountId(), board.getLocalId()); + b.getBoard().setArchived(true); + updateBoard(b); + liveData.postValue(b); + } catch (Throwable e) { + liveData.postError(e); + } }); + return liveData; } @AnyThread - public void dearchiveBoard(@NonNull Board board) { + public WrappedLiveData<FullBoard> dearchiveBoard(@NonNull Board board) { + WrappedLiveData<FullBoard> liveData = new WrappedLiveData<>(); doAsync(() -> { - FullBoard b = dataBaseAdapter.getFullBoardByLocalIdDirectly(board.getAccountId(), board.getLocalId()); - b.getBoard().setArchived(false); - updateBoard(b); + try { + FullBoard b = dataBaseAdapter.getFullBoardByLocalIdDirectly(board.getAccountId(), board.getLocalId()); + b.getBoard().setArchived(false); + updateBoard(b); + liveData.postValue(b); + } catch (Throwable e) { + liveData.postError(e); + } }); + return liveData; } @AnyThread @@ -1029,6 +1248,7 @@ public class SyncManager { liveData.postValue(dataBaseAdapter.getFullCardByLocalIdDirectly(card.getAccountId(), card.getLocalId())); } + @SuppressLint("MissingSuperCall") @Override public void onError(Throwable throwable) { liveData.postError(throwable); @@ -1076,7 +1296,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); @@ -1085,7 +1305,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); @@ -1094,7 +1316,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()); @@ -1108,8 +1334,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 { @@ -1152,10 +1380,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); } } @@ -1203,6 +1431,7 @@ public class SyncManager { liveData.postValue(response); } + @SuppressLint("MissingSuperCall") @Override public void onError(Throwable throwable) { liveData.postError(throwable); @@ -1214,14 +1443,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)); @@ -1291,6 +1524,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(); @@ -1301,8 +1539,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) { @@ -1364,6 +1602,7 @@ public class SyncManager { public LiveData<List<Label>> findProposalsForLabelsToAssign(final long accountId, final long boardId, long notAssignedToLocalCardId) { return dataBaseAdapter.findProposalsForLabelsToAssign(accountId, boardId, notAssignedToLocalCardId); } + public LiveData<List<Label>> findProposalsForLabelsToAssign(final long accountId, final long boardId) { return findProposalsForLabelsToAssign(accountId, boardId, -1L); } @@ -1389,8 +1628,8 @@ public class SyncManager { return new UserSearchLiveData(dataBaseAdapter, serverAdapter); } - public LiveData<Board> getBoard(long accountId, long remoteId) { - return dataBaseAdapter.getBoard(accountId, remoteId); + public LiveData<Board> getBoardByRemoteId(long accountId, long remoteId) { + return dataBaseAdapter.getBoardByRemoteId(accountId, remoteId); } public LiveData<Stack> getStackByRemoteId(long accountId, long localBoardId, long remoteId) { @@ -1424,7 +1663,7 @@ public class SyncManager { public void reorder(long accountId, @NonNull FullCard movedCard, long newStackId, int newIndex) { doAsync(() -> { // read cards of new stack - List<FullCard> cardsOfNewStack = dataBaseAdapter.getFullCardsForStackDirectly(accountId, newStackId); + List<FullCard> cardsOfNewStack = dataBaseAdapter.getFullCardsForStackDirectly(accountId, newStackId, null); int newOrder = newIndex; if (cardsOfNewStack.size() > newIndex) { newOrder = cardsOfNewStack.get(newIndex).getCard().getOrder(); @@ -1626,6 +1865,7 @@ public class SyncManager { liveData.postValue(response); } + @SuppressLint("MissingSuperCall") @Override public void onError(Throwable throwable) { liveData.postError(throwable); @@ -1638,15 +1878,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 @@ -1694,7 +1933,31 @@ public class SyncManager { doAsync(() -> dataBaseAdapter.deleteSingleCardWidget(widgetId)); } + public void addStackWidget(int appWidgetId, long accountId, long stackId, boolean darkTheme) { + doAsync(() -> dataBaseAdapter.createStackWidget(appWidgetId, accountId, stackId, darkTheme)); + } + + @WorkerThread + public StackWidgetModel getStackWidgetModelDirectly(int appWidgetId) throws NoSuchElementException { + final StackWidgetModel model = dataBaseAdapter.getStackWidgetModelDirectly(appWidgetId); + if (model == null) { + throw new NoSuchElementException(); + } + return model; + } + + public void deleteStackWidgetModel(int appWidgetId) { + doAsync(() -> dataBaseAdapter.deleteStackWidget(appWidgetId)); + } + private static class BooleanResultHolder { public boolean result = true; } + + /** + * FIXME https://github.com/stefan-niedermann/nextcloud-deck/issues/640 + */ + public static boolean ignoreExceptionOnVoidError(Throwable t) { + return t instanceof NullPointerException && "Attempt to invoke interface method 'void io.reactivex.disposables.Disposable.dispose()' on a null object reference".equals(t.getMessage()); + } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/ServerAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/ServerAdapter.java index 50110c20e..de32530bb 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/ServerAdapter.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/ServerAdapter.java @@ -13,6 +13,8 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.preference.PreferenceManager; +import com.nextcloud.android.sso.api.ParsedResponse; + import java.io.File; import java.text.DateFormat; import java.text.SimpleDateFormat; @@ -39,6 +41,9 @@ import it.niedermann.nextcloud.deck.model.full.FullStack; 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.projects.OcsProjectList; +import it.niedermann.nextcloud.deck.model.ocs.user.GroupMemberUIDs; +import it.niedermann.nextcloud.deck.model.ocs.user.OcsUser; import it.niedermann.nextcloud.deck.model.ocs.user.OcsUserList; import it.niedermann.nextcloud.deck.model.propagation.CardUpdate; import it.niedermann.nextcloud.deck.model.propagation.Reorder; @@ -148,14 +153,28 @@ public class ServerAdapter { responseCallback); } - public void getCapabilities(IResponseCallback<Capabilities> responseCallback) { + public void getCapabilities(String eTag, IResponseCallback<ParsedResponse<Capabilities>> responseCallback) { ensureInternetConnection(); - RequestHelper.request(provider, () -> provider.getNextcloudAPI().getCapabilities(), responseCallback); + RequestHelper.request(provider, () -> provider.getNextcloudAPI().getCapabilities(eTag), responseCallback); + } + + public void getProjectsForCard(long remoteCardId, IResponseCallback<OcsProjectList> responseCallback) { + ensureInternetConnection(); + RequestHelper.request(provider, () -> provider.getNextcloudAPI().getProjectsForCard(remoteCardId), responseCallback); } public void searchUser(String searchTerm, IResponseCallback<OcsUserList> responseCallback) { ensureInternetConnection(); RequestHelper.request(provider, () -> provider.getNextcloudAPI().searchUser(searchTerm), responseCallback); } + public void getSingleUserData(String userUid, IResponseCallback<OcsUser> responseCallback) { + ensureInternetConnection(); + RequestHelper.request(provider, () -> provider.getNextcloudAPI().getSingleUserData(userUid), responseCallback); + } + + public void searchGroupMembers(String groupUID, IResponseCallback<GroupMemberUIDs> responseCallback) { + ensureInternetConnection(); + RequestHelper.request(provider, () -> provider.getNextcloudAPI().searchGroupMembers(groupUID), responseCallback); + } public void getActivitiesForCard(long cardId, IResponseCallback<List<it.niedermann.nextcloud.deck.model.ocs.Activity>> responseCallback) { ensureInternetConnection(); 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 9913c0ba2..f08cbe2a1 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 @@ -2,6 +2,7 @@ package it.niedermann.nextcloud.deck.persistence.sync.adapters.db; import android.content.Context; +import androidx.annotation.AnyThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; @@ -24,10 +25,12 @@ import it.niedermann.nextcloud.deck.model.JoinCardWithUser; import it.niedermann.nextcloud.deck.model.Label; import it.niedermann.nextcloud.deck.model.Stack; import it.niedermann.nextcloud.deck.model.User; +import it.niedermann.nextcloud.deck.model.appwidgets.StackWidgetModel; import it.niedermann.nextcloud.deck.model.enums.DBStatus; import it.niedermann.nextcloud.deck.model.enums.EDueType; import it.niedermann.nextcloud.deck.model.full.FullBoard; import it.niedermann.nextcloud.deck.model.full.FullCard; +import it.niedermann.nextcloud.deck.model.full.FullCardWithProjects; import it.niedermann.nextcloud.deck.model.full.FullSingleCardWidgetModel; import it.niedermann.nextcloud.deck.model.full.FullStack; import it.niedermann.nextcloud.deck.model.interfaces.AbstractRemoteEntity; @@ -37,10 +40,16 @@ import it.niedermann.nextcloud.deck.model.ocs.Activity; import it.niedermann.nextcloud.deck.model.ocs.comment.DeckComment; import it.niedermann.nextcloud.deck.model.ocs.comment.Mention; import it.niedermann.nextcloud.deck.model.ocs.comment.full.FullDeckComment; +import it.niedermann.nextcloud.deck.model.ocs.projects.JoinCardWithProject; +import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProject; +import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProjectResource; +import it.niedermann.nextcloud.deck.model.relations.UserInBoard; +import it.niedermann.nextcloud.deck.model.relations.UserInGroup; import it.niedermann.nextcloud.deck.model.widget.singlecard.SingleCardWidgetModel; 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.widget.singlecard.SingleCardWidget; +import it.niedermann.nextcloud.deck.ui.widget.stack.StackWidget; import static androidx.lifecycle.Transformations.distinctUntilChanged; @@ -76,7 +85,7 @@ public class DataBaseAdapter { return LiveDataHelper.postCustomValue(db.getAccountDao().countAccounts(), data -> data != null && data > 0); } - public LiveData<Board> getBoard(long accountId, long remoteId) { + public LiveData<Board> getBoardByRemoteId(long accountId, long remoteId) { return distinctUntilChanged(db.getBoardDao().getBoardByRemoteId(accountId, remoteId)); } @@ -178,24 +187,66 @@ public class DataBaseAdapter { if (filter == null) { return LiveDataHelper.interceptLiveData(db.getCardDao().getFullCardsForStack(accountId, localStackId), this::filterRelationsForCard); } + return LiveDataHelper.interceptLiveData(db.getCardDao().getFilteredFullCardsForStack(getQueryForFilter(filter, accountId, localStackId)), this::filterRelationsForCard); + } + + private void fillSqlWithListValues(StringBuilder query, List<Object> args, @NonNull List<? extends IRemoteEntity> entities) { + for (int i = 0; i < entities.size(); i++) { + if (i > 0) { + query.append(", "); + } + query.append("?"); + args.add(entities.get(i).getLocalId()); + } + } + + @WorkerThread + public List<FullCard> getFullCardsForStackDirectly(long accountId, long localStackId, FilterInformation filter) { + if (filter == null) { + return db.getCardDao().getFullCardsForStackDirectly(accountId, localStackId); + } List<Object> args = new ArrayList<>(); - StringBuilder query = new StringBuilder("SELECT * FROM card c " + - "WHERE accountId = ? AND stackId = ? "); args.add(accountId); args.add(localStackId); + return db.getCardDao().getFilteredFullCardsForStackDirectly(getQueryForFilter(filter, accountId, localStackId)); + } + + @AnyThread + private SimpleSQLiteQuery getQueryForFilter(FilterInformation filter, long accountId, long localStackId) { + List<Object> args = new ArrayList<>(); + args.add(accountId); + args.add(localStackId); + StringBuilder query = new StringBuilder("SELECT * FROM card c " + + "WHERE accountId = ? AND stackId = ? "); + if (!filter.getLabels().isEmpty()) { - query.append("and exists(select 1 from joincardwithlabel j where c.localId = cardId and labelId in ("); + query.append("and (exists(select 1 from joincardwithlabel j where c.localId = cardId and labelId in ("); fillSqlWithListValues(query, args, filter.getLabels()); query.append(") and j.status<>3) "); + if (filter.isNoAssignedLabel()) { + query.append("or not exists(select 1 from joincardwithlabel j where c.localId = cardId and j.status<>3)) "); + } else { + query.append(") "); + } + } else if (filter.isNoAssignedLabel()) { + query.append("and not exists(select 1 from joincardwithlabel j where c.localId = cardId and j.status<>3) "); } if (!filter.getUsers().isEmpty()) { - query.append("and exists(select 1 from joincardwithuser j where c.localId = cardId and userId in ("); + query.append("and (exists(select 1 from joincardwithuser j where c.localId = cardId and userId in ("); fillSqlWithListValues(query, args, filter.getUsers()); query.append(") and j.status<>3) "); + if (filter.isNoAssignedUser()) { + query.append("or not exists(select 1 from joincardwithuser j where c.localId = cardId and j.status<>3)) "); + } else { + query.append(") "); + } + } else if (filter.isNoAssignedUser()) { + query.append("and not exists(select 1 from joincardwithuser j where c.localId = cardId and j.status<>3) "); } + if (filter.getDueType() != EDueType.NO_FILTER) { switch (filter.getDueType()) { case NO_DUE: @@ -217,24 +268,11 @@ public class DataBaseAdapter { throw new IllegalArgumentException("Xou need to add your new EDueType value\"" + filter.getDueType() + "\" here!"); } } - query.append(" and status<>3 order by `order`, createdAt asc;"); - return LiveDataHelper.interceptLiveData(db.getCardDao().getFilteredFullCardsForStack(new SimpleSQLiteQuery(query.toString(), args.toArray())), this::filterRelationsForCard); - - } - - private void fillSqlWithListValues(StringBuilder query, List<Object> args, @NonNull List<? extends IRemoteEntity> entities) { - for (int i = 0; i < entities.size(); i++) { - if (i > 0) { - query.append(", "); - } - query.append("?"); - args.add(entities.get(i).getLocalId()); + if (filter.getArchiveStatus() != FilterInformation.EArchiveStatus.ALL) { + query.append(" and c.archived = "+(filter.getArchiveStatus() == FilterInformation.EArchiveStatus.ARCHIVED ? 1 : 0)); } - } - - @WorkerThread - public List<FullCard> getFullCardsForStackDirectly(long accountId, long localStackId) { - return db.getCardDao().getFullCardsForStackDirectly(accountId, localStackId); + query.append(" and status<>3 order by `order`, createdAt asc;"); + return new SimpleSQLiteQuery(query.toString(), args.toArray()); } @WorkerThread @@ -242,17 +280,20 @@ public class DataBaseAdapter { return db.getUserDao().getUserByUidDirectly(accountId, uid); } + @WorkerThread public long createUser(long accountId, User user) { user.setAccountId(accountId); return db.getUserDao().insert(user); } + @WorkerThread public void updateUser(long accountId, User user, boolean setStatus) { markAsEditedIfNeeded(user, setStatus); user.setAccountId(accountId); db.getUserDao().update(user); } + @AnyThread public LiveData<Label> getLabelByRemoteId(long accountId, long remoteId) { return distinctUntilChanged(db.getLabelDao().getLabelByRemoteId(accountId, remoteId)); } @@ -262,7 +303,8 @@ public class DataBaseAdapter { return db.getLabelDao().getLabelByRemoteIdDirectly(accountId, remoteId); } - public long createLabel(long accountId, @NonNull Label label) { + @WorkerThread + public long createLabelDirectly(long accountId, @NonNull Label label) { label.setAccountId(accountId); return db.getLabelDao().insert(label); } @@ -318,6 +360,8 @@ public class DataBaseAdapter { // readded! existing.setStatusEnum(DBStatus.LOCAL_EDITED); db.getJoinCardWithUserDao().update(existing); + } else if (existing != null) { + return; } else { JoinCardWithUser join = new JoinCardWithUser(); join.setCardId(localCardId); @@ -341,6 +385,25 @@ public class DataBaseAdapter { public void deleteJoinedLabelsForBoard(Long localBoardId) { db.getJoinBoardWithLabelDao().deleteByBoardId(localBoardId); } + public void deleteGroupMembershipsOfGroup(Long localGroupUserId) { + db.getUserInGroupDao().deleteByGroupId(localGroupUserId); + } + public void deleteBoardMembershipsOfBoard(Long localBoardId) { + db.getUserInBoardDao().deleteByBoardId(localBoardId); + } + public void addUserToGroup(Long localGroupUserId, Long localGroupMemberId) { + UserInGroup relation = new UserInGroup(); + relation.setGroupId(localGroupUserId); + relation.setMemberId(localGroupMemberId); + db.getUserInGroupDao().insert(relation); + } + + public void addUserToBoard(Long localUserId, Long localBoardId) { + UserInBoard relation = new UserInBoard(); + relation.setBoardId(localBoardId); + relation.setUserId(localUserId); + db.getUserInBoardDao().insert(relation); + } public void updateLabel(Label label, boolean setStatus) { markAsEditedIfNeeded(label, setStatus); @@ -389,6 +452,10 @@ public class DataBaseAdapter { return distinctUntilChanged(db.getAccountDao().getAllAccounts()); } + public LiveData<List<Board>> getBoards(long accountId) { + return distinctUntilChanged(db.getBoardDao().getBoardsForAccount(accountId)); + } + public LiveData<List<Board>> getBoards(long accountId, boolean archived) { return distinctUntilChanged( archived @@ -429,8 +496,8 @@ public class DataBaseAdapter { db.getBoardDao().update(board); } - public LiveData<List<FullStack>> getFullStacksForBoard(long accountId, long localBoardId) { - return distinctUntilChanged(db.getStackDao().getFullStacksForBoard(accountId, localBoardId)); + public LiveData<List<Stack>> getStacksForBoard(long accountId, long localBoardId) { + return distinctUntilChanged(db.getStackDao().getStacksForBoard(accountId, localBoardId)); } @WorkerThread @@ -438,27 +505,36 @@ public class DataBaseAdapter { return db.getStackDao().getFullStacksForBoardDirectly(accountId, localBoardId); } + @AnyThread public LiveData<FullStack> getStack(long accountId, long localStackId) { return distinctUntilChanged(db.getStackDao().getFullStack(accountId, localStackId)); } + @WorkerThread public long createStack(long accountId, Stack stack) { stack.setAccountId(accountId); return db.getStackDao().insert(stack); } + @WorkerThread public void deleteStack(Stack stack, boolean setStatus) { markAsDeletedIfNeeded(stack, setStatus); db.getStackDao().update(stack); } + @WorkerThread public void deleteStackPhysically(Stack stack) { db.getStackDao().delete(stack); } + @WorkerThread public void updateStack(Stack stack, boolean setStatus) { markAsEditedIfNeeded(stack, setStatus); db.getStackDao().update(stack); + if (db.getStackWidgetModelDao().containsStackLocalId(stack.getLocalId())) { + DeckLog.info("Notifying " + StackWidget.class.getSimpleName() + " about card changes for \"" + stack.getTitle() + "\""); + StackWidget.notifyDatasetChanged(context); + } } @WorkerThread @@ -466,10 +542,16 @@ public class DataBaseAdapter { return db.getCardDao().getCardByLocalIdDirectly(accountId, localCardId); } + @AnyThread public LiveData<FullCard> getCardByLocalId(long accountId, long localCardId) { return LiveDataHelper.interceptLiveData(db.getCardDao().getFullCardByLocalId(accountId, localCardId), this::filterRelationsForCard); } + @AnyThread + public LiveData<FullCardWithProjects> getCardWithProjectsByLocalId(long accountId, long localCardId) { + return LiveDataHelper.interceptLiveData(db.getCardDao().getFullCardWithProjectsByLocalId(accountId, localCardId), this::filterRelationsForCard); + } + @WorkerThread public List<FullCard> getLocallyChangedCardsDirectly(long accountId) { return db.getCardDao().getLocallyChangedCardsDirectly(accountId); @@ -480,15 +562,27 @@ public class DataBaseAdapter { return db.getCardDao().getLocallyChangedCardsByLocalStackIdDirectly(accountId, localStackId); } - public long createCard(long accountId, Card card) { + @WorkerThread + public long createCardDirectly(long accountId, Card card) { card.setAccountId(accountId); - return db.getCardDao().insert(card); + long newCardId = db.getCardDao().insert(card); + + notifyStackWidgetsIfNeeded(card.getTitle(), card.getStackId()); + + return newCardId; } - public int getHighestCardOrderInStack(long localStackId){ + @WorkerThread + public int getHighestCardOrderInStack(long localStackId) { return db.getCardDao().getHighestOrderInStack(localStackId); } + @WorkerThread + public int getHighestStackOrderInBoard(long localBoardId) { + return db.getStackDao().getHighestStackOrderInBoard(localBoardId); + } + + @WorkerThread public void deleteCard(Card card, boolean setStatus) { markAsDeletedIfNeeded(card, setStatus); if (setStatus) { @@ -496,21 +590,35 @@ public class DataBaseAdapter { } else { deleteCardPhysically(card); } + + notifyStackWidgetsIfNeeded(card.getTitle(), card.getStackId()); } + @WorkerThread public void deleteCardPhysically(Card card) { db.getCardDao().delete(card); } + @WorkerThread public void updateCard(@NonNull Card card, boolean setStatus) { markAsEditedIfNeeded(card, setStatus); + Long originalStackLocalId = db.getCardDao().getLocalStackIdByLocalCardId(card.getLocalId()); db.getCardDao().update(card); if (db.getSingleCardWidgetModelDao().containsCardLocalId(card.getLocalId())) { - DeckLog.info("Notifying widget about card changes for \"" + card.getTitle() + "\""); + DeckLog.info("Notifying " + SingleCardWidget.class.getSimpleName() + " about card changes for \"" + card.getTitle() + "\""); SingleCardWidget.notifyDatasetChanged(context); } + notifyStackWidgetsIfNeeded(card.getTitle(), card.getStackId(), originalStackLocalId); } + private void notifyStackWidgetsIfNeeded(String cardTitle, long... affectedStackIds) { + if (db.getStackWidgetModelDao().containsStackLocalId(affectedStackIds)) { + DeckLog.info("Notifying " + StackWidget.class.getSimpleName() + " about card changes for \"" + cardTitle + "\""); + StackWidget.notifyDatasetChanged(context); + } + } + + @WorkerThread public long createAccessControl(long accountId, @NonNull AccessControl entity) { entity.setAccountId(accountId); return db.getAccessControlDao().insert(entity); @@ -569,10 +677,6 @@ public class DataBaseAdapter { return db.getUserDao().searchUserByUidOrDisplayName(accountId, boardId, notYetAssignedToLocalCardId, "%" + searchTerm.trim() + "%"); } - public LiveData<List<User>> searchUserByUidOrDisplayNameForACL(final long accountId, final long notYetAssignedToACL, final String searchTerm) { - validateSearchTerm(searchTerm); - return db.getUserDao().searchUserByUidOrDisplayNameForACL(accountId, notYetAssignedToACL, "%" + searchTerm.trim() + "%"); - } public List<User> searchUserByUidOrDisplayNameForACLDirectly(final long accountId, final long notYetAssignedToACL, final String searchTerm) { validateSearchTerm(searchTerm); return db.getUserDao().searchUserByUidOrDisplayNameForACLDirectly(accountId, notYetAssignedToACL, "%" + searchTerm.trim() + "%"); @@ -620,6 +724,11 @@ public class DataBaseAdapter { return db.getAttachmentDao().getLocallyChangedAttachmentsDirectly(accountId); } + @WorkerThread + public List<Attachment> getLocallyChangedAttachmentsForStackDirectly(long localStackId) { + return db.getAttachmentDao().getLocallyChangedAttachmentsForStackDirectly(localStackId); + } + public long createAttachment(long accountId, @NonNull Attachment attachment) { attachment.setAccountId(accountId); attachment.setCreatedAt(new Date()); @@ -722,16 +831,24 @@ public class DataBaseAdapter { return db.getJoinCardWithLabelDao().getAllDeletedJoinsWithRemoteIDs(); } - public List<JoinCardWithLabel> getAllChangedJoins() { + public List<JoinCardWithLabel> getAllChangedLabelJoins() { return db.getJoinCardWithLabelDao().getAllChangedJoins(); } - public JoinCardWithLabel getRemoteIdsForJoin(Long localCardId, Long localLabelId) { + public List<JoinCardWithLabel> getAllChangedLabelJoinsForStack(Long localStackId) { + return db.getJoinCardWithLabelDao().getAllChangedJoinsForStack(localStackId); + } + + public JoinCardWithLabel getAllChangedLabelJoinsWithRemoteIDs(Long localCardId, Long localLabelId) { return db.getJoinCardWithLabelDao().getRemoteIdsForJoin(localCardId, localLabelId); } - public List<JoinCardWithUser> getAllDeletedUserJoinsWithRemoteIDs() { - return db.getJoinCardWithUserDao().getDeletedJoinsWithRemoteIDs(); + public List<JoinCardWithUser> getAllChangedUserJoinsWithRemoteIDs() { + return db.getJoinCardWithUserDao().getChangedJoinsWithRemoteIDs(); + } + + public List<JoinCardWithUser> getAllChangedUserJoinsWithRemoteIDsForStack(Long localStackId) { + return db.getJoinCardWithUserDao().getChangedJoinsWithRemoteIDsForStack(localStackId); } public void deleteJoinedLabelForCardPhysicallyByRemoteIDs(Long accountId, Long remoteCardId, Long remoteLabelId) { @@ -839,11 +956,17 @@ public class DataBaseAdapter { return db.getCommentDao().getCommentByLocalCardIdDirectly(localCardId); } + @WorkerThread public List<Card> getCardsWithLocallyChangedCommentsDirectly(Long accountId) { return db.getCardDao().getCardsWithLocallyChangedCommentsDirectly(accountId); } @WorkerThread + public List<Card> getCardsWithLocallyChangedCommentsForStackDirectly(Long localStackId) { + return db.getCardDao().getCardsWithLocallyChangedCommentsForStackDirectly(localStackId); + } + + @WorkerThread public Long getLocalStackIdByRemoteStackIdDirectly(long accountId, Long stackId) { return db.getStackDao().getLocalStackIdByRemoteStackIdDirectly(accountId, stackId); } @@ -912,11 +1035,86 @@ public class DataBaseAdapter { db.getSingleCardWidgetModelDao().delete(model); } + public long createStackWidget(int appWidgetId, long accountId, long stackId, boolean darkTheme) { + StackWidgetModel model = new StackWidgetModel(); + model.setAppWidgetId(appWidgetId); + model.setAccountId(accountId); + model.setStackId(stackId); + model.setDarkTheme(darkTheme); + + return db.getStackWidgetModelDao().insert(model); + } + + public StackWidgetModel getStackWidgetModelDirectly(int appWidgetId) { + return db.getStackWidgetModelDao().getStackWidgetByAppWidgetIdDirectly(appWidgetId); + } + + public void deleteStackWidget(int appWidgetId) { + StackWidgetModel model = new StackWidgetModel(); + model.setAppWidgetId(appWidgetId); + db.getStackWidgetModelDao().delete(model); + } + public LiveData<List<Account>> readAccountsForHostWithReadAccessToBoard(String host, long boardRemoteId) { - return db.getAccountDao().readAccountsForHostWithReadAccessToBoard("%"+host+"%", boardRemoteId); + return db.getAccountDao().readAccountsForHostWithReadAccessToBoard("%" + host + "%", boardRemoteId); } public List<Account> readAccountsForHostWithReadAccessToBoardDirectly(String host, long boardRemoteId) { - return db.getAccountDao().readAccountsForHostWithReadAccessToBoardDirectly("%"+host+"%", boardRemoteId); + return db.getAccountDao().readAccountsForHostWithReadAccessToBoardDirectly("%" + host + "%", boardRemoteId); + } + + public Board getBoardForAccountByNameDirectly(long account, String title) { + return db.getBoardDao().getBoardForAccountByNameDirectly(account, title); + } + + public OcsProject getProjectByRemoteIdDirectly(long accountId, Long remoteId) { + return db.getOcsProjectDao().getProjectByRemoteIdDirectly(accountId, remoteId); + } + + public Long createProjectDirectly(long accountId, OcsProject entity) { + entity.setAccountId(accountId); + return db.getOcsProjectDao().insert(entity); + } + + public void deleteProjectResourcesForProjectIdDirectly(Long localProjectId) { + db.getOcsProjectResourceDao().deleteByProjectId(localProjectId); + } + + public void updateProjectDirectly(long accountId, OcsProject entity) { + entity.setAccountId(accountId); + db.getOcsProjectDao().update(entity); + } + + public void deleteProjectDirectly(OcsProject ocsProject) { + db.getOcsProjectResourceDao().deleteByProjectId(ocsProject.getLocalId()); + db.getOcsProjectDao().delete(ocsProject); + } + + public Long createProjectResourceDirectly(Long accountId, OcsProjectResource resource) { + resource.setAccountId(accountId); + return db.getOcsProjectResourceDao().insert(resource); + } + public int countProjectResourcesInProjectDirectly(Long projectLocalId) { + return db.getOcsProjectResourceDao().countProjectResourcesInProjectDirectly(projectLocalId); + } + public LiveData<Integer> countProjectResourcesInProject(Long projectLocalId) { + return db.getOcsProjectResourceDao().countProjectResourcesInProject(projectLocalId); + } + public LiveData<List<OcsProjectResource>> getResourcesByLocalProjectId(Long projectLocalId) { + return db.getOcsProjectResourceDao().getResourcesByLocalProjectId(projectLocalId); + } + + public void assignCardToProjectIfMissng(Long accountId, Long localProjectId, Long remoteCardId) { + Card card = db.getCardDao().getCardByRemoteIdDirectly(accountId, remoteCardId); + if (card != null) { + JoinCardWithProject existing = db.getJoinCardWithOcsProjectDao().getAssignmentByCardIdAndProjectIdDirectly(card.getLocalId(), localProjectId); + if (existing == null) { + JoinCardWithProject assignment = new JoinCardWithProject(); + assignment.setStatus(DBStatus.UP_TO_DATE.getId()); + assignment.setCardId(card.getLocalId()); + assignment.setProjectId(localProjectId); + db.getJoinCardWithOcsProjectDao().insert(assignment); + } + } } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/DeckDatabase.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/DeckDatabase.java index 0e86557fa..d2106f03b 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/DeckDatabase.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/DeckDatabase.java @@ -2,7 +2,9 @@ package it.niedermann.nextcloud.deck.persistence.sync.adapters.db; import android.content.Context; import android.database.Cursor; +import android.graphics.Color; +import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.preference.PreferenceManager; import androidx.room.Database; @@ -12,6 +14,7 @@ import androidx.room.TypeConverters; import androidx.room.migration.Migration; import androidx.sqlite.db.SupportSQLiteDatabase; +import it.niedermann.android.util.ColorUtil; import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.api.LastSyncUtil; import it.niedermann.nextcloud.deck.model.AccessControl; @@ -28,10 +31,16 @@ import it.niedermann.nextcloud.deck.model.Label; import it.niedermann.nextcloud.deck.model.Permission; import it.niedermann.nextcloud.deck.model.Stack; import it.niedermann.nextcloud.deck.model.User; +import it.niedermann.nextcloud.deck.model.appwidgets.StackWidgetModel; import it.niedermann.nextcloud.deck.model.enums.DBStatus; import it.niedermann.nextcloud.deck.model.ocs.Activity; import it.niedermann.nextcloud.deck.model.ocs.comment.DeckComment; import it.niedermann.nextcloud.deck.model.ocs.comment.Mention; +import it.niedermann.nextcloud.deck.model.ocs.projects.JoinCardWithProject; +import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProject; +import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProjectResource; +import it.niedermann.nextcloud.deck.model.relations.UserInBoard; +import it.niedermann.nextcloud.deck.model.relations.UserInGroup; import it.niedermann.nextcloud.deck.model.widget.singlecard.SingleCardWidgetModel; import it.niedermann.nextcloud.deck.persistence.sync.SyncWorker; import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.AccessControlDao; @@ -49,9 +58,15 @@ import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.JoinCardWit import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.LabelDao; import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.MentionDao; import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.PermissionDao; -import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.SingleCardWidgetModelDao; import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.StackDao; import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.UserDao; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.UserInBoardDao; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.UserInGroupDao; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.projects.JoinCardWithOcsProjectDao; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.projects.OcsProjectDao; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.projects.OcsProjectResourceDao; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.widgets.SingleCardWidgetModelDao; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.widgets.StackWidgetModelDao; @Database( entities = { @@ -73,9 +88,15 @@ import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.UserDao; DeckComment.class, Mention.class, SingleCardWidgetModel.class, + StackWidgetModel.class, + OcsProject.class, + OcsProjectResource.class, + JoinCardWithProject.class, + UserInGroup.class, + UserInBoard.class, }, exportSchema = false, - version = 15 + version = 21 ) @TypeConverters({DateTypeConverter.class}) public abstract class DeckDatabase extends RoomDatabase { @@ -175,8 +196,185 @@ public abstract class DeckDatabase extends RoomDatabase { } }; - public static final RoomDatabase.Callback ON_CREATE_CALLBACK = new RoomDatabase.Callback() { + private static final Migration MIGRATION_15_16 = new Migration(15, 16) { + @Override + public void migrate(SupportSQLiteDatabase database) { + database.execSQL("CREATE TABLE `StackWidgetModel` (`appWidgetId` INTEGER PRIMARY KEY, `accountId` INTEGER, `stackId` INTEGER, `darkTheme` INTEGER CHECK (`darkTheme` IN (0,1)) NOT NULL, " + + "FOREIGN KEY(`accountId`) REFERENCES `Account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE, " + + "FOREIGN KEY(`stackId`) REFERENCES `Stack`(`localId`) ON UPDATE NO ACTION ON DELETE CASCADE )"); + database.execSQL("CREATE INDEX `index_StackWidgetModel_stackId` ON `StackWidgetModel` (`stackId`)"); + database.execSQL("CREATE INDEX `index_StackWidgetModel_accountId` ON `StackWidgetModel` (`accountId`)"); + } + }; + + private static final Migration MIGRATION_16_17 = new Migration(16, 17) { + @Override + public void migrate(SupportSQLiteDatabase database) { + database.execSQL("CREATE TABLE `OcsProject` (`localId` INTEGER PRIMARY KEY AUTOINCREMENT, `accountId` INTEGER NOT NULL, `id` INTEGER, `name` TEXT NOT NULL, `status` INTEGER NOT NULL, `lastModified` INTEGER, `lastModifiedLocal` INTEGER)"); + database.execSQL("CREATE UNIQUE INDEX `index_OcsProject_accountId_id` ON `OcsProject` (`accountId`, `id`)"); + database.execSQL("CREATE INDEX `index_project_accID` ON `OcsProject` (`accountId`)"); + database.execSQL("CREATE INDEX `index_OcsProject_id` ON `OcsProject` (`id`)"); + database.execSQL("CREATE INDEX `index_OcsProject_lastModifiedLocal` ON `OcsProject` (`lastModifiedLocal`)"); + + database.execSQL("CREATE TABLE `OcsProjectResource` (`localId` INTEGER PRIMARY KEY AUTOINCREMENT, `accountId` INTEGER NOT NULL, `id` INTEGER, `name` TEXT, `status` INTEGER NOT NULL, `lastModified` INTEGER, `lastModifiedLocal` INTEGER, `projectId` INTEGER NOT NULL, `type` TEXT , `link` TEXT , `path` TEXT, `iconUrl` TEXT , `previewAvailable` INTEGER, `mimetype` TEXT, FOREIGN KEY(`projectId`) REFERENCES `OcsProject`(`localId`) ON UPDATE NO ACTION ON DELETE CASCADE)"); + database.execSQL("CREATE INDEX `index_projectResource_accID` ON `OcsProjectResource` (`accountId`)"); + database.execSQL("CREATE INDEX `index_projectResource_projectId` ON `OcsProjectResource` (`projectId`)"); + database.execSQL("CREATE UNIQUE INDEX `index_OcsProjectResource_accountId_id` ON `OcsProjectResource` (`accountId`, `id`, `projectId`)"); + database.execSQL("CREATE INDEX `index_OcsProjectResource_id` ON `OcsProjectResource` (`id`)"); + database.execSQL("CREATE INDEX `index_OcsProjectResource_lastModifiedLocal` ON `OcsProjectResource` (`lastModifiedLocal`)"); + + database.execSQL("CREATE TABLE `JoinCardWithProject` (`status` INTEGER NOT NULL, `projectId` INTEGER NOT NULL, `cardId` INTEGER NOT NULL, PRIMARY KEY (`projectId`, `cardId`), FOREIGN KEY(`cardId`) REFERENCES `Card`(`localId`) ON UPDATE NO ACTION ON DELETE CASCADE, FOREIGN KEY(`projectId`) REFERENCES `OcsProject`(`localId`) ON UPDATE NO ACTION ON DELETE CASCADE)"); + database.execSQL("CREATE INDEX `index_JoinCardWithProject_projectId` ON `JoinCardWithProject` (`projectId`)"); + database.execSQL("CREATE INDEX `index_JoinCardWithProject_cardId` ON `JoinCardWithProject` (`cardId`)"); + } + }; + + private static final Migration MIGRATION_17_18 = new Migration(17, 18) { + @Override + public void migrate(SupportSQLiteDatabase database) { + // https://github.com/stefan-niedermann/nextcloud-deck/issues/435 + database.execSQL("ALTER TABLE `Account` ADD `etag` TEXT"); + } + }; + private static final Migration MIGRATION_18_19 = new Migration(18, 19) { + @Override + public void migrate(SupportSQLiteDatabase database) { + // https://github.com/stefan-niedermann/nextcloud-deck/issues/619 + database.execSQL("DROP INDEX `index_OcsProjectResource_accountId_id`"); + database.execSQL("ALTER TABLE `OcsProjectResource` ADD `idString` TEXT"); + database.execSQL("CREATE UNIQUE INDEX `index_OcsProjectResource_accountId_id` ON `OcsProjectResource` (`accountId`, `id`, `idString`, `projectId`)"); + } + }; + private static final Migration MIGRATION_19_20 = new Migration(19, 20) { + @Override + public void migrate(SupportSQLiteDatabase database) { + // https://github.com/stefan-niedermann/nextcloud-deck/issues/492 + // https://github.com/stefan-niedermann/nextcloud-deck/issues/631 + database.execSQL("CREATE TABLE `UserInGroup` (`groupId` INTEGER NOT NULL, `memberId` INTEGER NOT NULL, " + + "primary KEY(`groupId`, `memberId`), " + + "FOREIGN KEY(`groupId`) REFERENCES `User`(`localId`) ON UPDATE NO ACTION ON DELETE CASCADE, " + + "FOREIGN KEY(`memberId`) REFERENCES `User`(`localId`) ON UPDATE NO ACTION ON DELETE CASCADE)"); + database.execSQL("CREATE UNIQUE INDEX `unique_idx_group_member` ON `UserInGroup` (`groupId`, `memberId`)"); + database.execSQL("CREATE INDEX `index_UserInGroup_groupId` ON `UserInGroup` (`groupId`)"); + database.execSQL("CREATE INDEX `index_UserInGroup_memberId` ON `UserInGroup` (`memberId`)"); + + database.execSQL("CREATE TABLE `UserInBoard` (`userId` INTEGER NOT NULL, `boardId` INTEGER NOT NULL, " + + "primary KEY(`userId`, `boardId`), " + + "FOREIGN KEY(`userId`) REFERENCES `User`(`localId`) ON UPDATE NO ACTION ON DELETE CASCADE, " + + "FOREIGN KEY(`boardId`) REFERENCES `Board`(`localId`) ON UPDATE NO ACTION ON DELETE CASCADE)"); + database.execSQL("CREATE UNIQUE INDEX `unique_idx_user_board` ON `UserInBoard` (`userId`, `boardId`)"); + database.execSQL("CREATE INDEX `index_UserInBoard_userId` ON `UserInBoard` (`userId`)"); + database.execSQL("CREATE INDEX `index_UserInBoard_boardId` ON `UserInBoard` (`boardId`)"); + } + }; + + private static final Migration MIGRATION_20_21 = new Migration(20, 21) { + @Override + public void migrate(SupportSQLiteDatabase database) { + // https://github.com/stefan-niedermann/nextcloud-deck/issues/556 + String suffix = "_new"; + { + String tableName = "Account"; + database.execSQL("CREATE TABLE `" + tableName + suffix + "` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT NOT NULL, `userName` TEXT NOT NULL, `url` TEXT NOT NULL, " + + "`color` INTEGER NOT NULL DEFAULT 0, `textColor` INTEGER NOT NULL DEFAULT 0, `serverDeckVersion` TEXT NOT NULL DEFAULT '0.6.4', `maintenanceEnabled` INTEGER NOT NULL DEFAULT 0, `etag` TEXT)"); + Cursor cursor = database.query("select * from `" + tableName + "`"); + while (cursor.moveToNext()) { + String colorAsString1 = cursor.getString(4); // color + String colorAsString2 = cursor.getString(5); // textColor + + @ColorInt Integer color1 = null; + @ColorInt Integer color2 = null; + try { + color1 = Color.parseColor(ColorUtil.INSTANCE.formatColorToParsableHexString(colorAsString1)); + color2 = Color.parseColor(ColorUtil.INSTANCE.formatColorToParsableHexString(colorAsString2)); + } catch (Exception e) { + color1 = Color.GRAY; + color2 = Color.GRAY; + } + database.execSQL("Insert into `" + tableName + suffix + "` VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", new Object[]{ + cursor.getLong(0), cursor.getString(1), cursor.getString(2), cursor.getString(3), + color1, color2, cursor.getString(6), cursor.getInt(7), cursor.getString(8)}); + + } + + + database.execSQL("DROP TABLE `" + tableName + "`"); + database.execSQL("ALTER TABLE `" + tableName + suffix + "` RENAME TO `" + tableName + "`"); + database.execSQL("CREATE UNIQUE INDEX `index_Account_name` ON `" + tableName + "` (`name`)"); + database.execSQL("UPDATE SQLITE_SEQUENCE SET seq = (select max(id) from " + tableName + ") WHERE name = ?", new Object[]{tableName}); + } + { + String tableName = "Board"; + database.execSQL("CREATE TABLE `" + tableName + suffix + "` (`localId` INTEGER PRIMARY KEY AUTOINCREMENT, `accountId` INTEGER NOT NULL, `id` INTEGER, `status` INTEGER NOT NULL, " + + "`lastModified` INTEGER, `lastModifiedLocal` INTEGER, `title` TEXT, `ownerId` INTEGER NOT NULL, `color` INTEGER, " + + "`archived` INTEGER NOT NULL, `shared` INTEGER NOT NULL, `deletedAt` INTEGER, `permissionRead` INTEGER NOT NULL, " + + "`permissionEdit` INTEGER NOT NULL, `permissionManage` INTEGER NOT NULL, `permissionShare` INTEGER NOT NULL, " + + "FOREIGN KEY(`ownerId`) REFERENCES `User`(`localId`) ON UPDATE NO ACTION ON DELETE SET NULL )"); + Cursor cursor = database.query("select * from `" + tableName + "`"); + while (cursor.moveToNext()) { + String colorAsString1 = cursor.getString(8); // color + + @ColorInt Integer color1 = null; + try { + color1 = Color.parseColor(ColorUtil.INSTANCE.formatColorToParsableHexString(colorAsString1)); + } catch (Exception e) { + color1 = Color.GRAY; + } + database.execSQL("Insert into `" + tableName + suffix + "` VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", new Object[]{ + cursor.getLong(0), cursor.getLong(1), cursor.getLong(2), cursor.getInt(3), + cursor.getLong(4), cursor.getLong(5), cursor.getString(6), cursor.getLong(7), color1, + cursor.getInt(9), cursor.getInt(10), cursor.getInt(11), cursor.getInt(12), + cursor.getInt(13), cursor.getInt(14), cursor.getInt(15) + }); + + } + + + database.execSQL("DROP TABLE `" + tableName + "`"); + database.execSQL("ALTER TABLE `" + tableName + suffix + "` RENAME TO `" + tableName + "`"); + database.execSQL("CREATE INDEX `index_Board_accountId` ON `" + tableName + "` (`accountId`)"); + database.execSQL("CREATE UNIQUE INDEX `index_Board_accountId_id` ON `" + tableName + "` (`accountId`, `id`)"); + database.execSQL("CREATE INDEX `index_Board_id` ON `" + tableName + "` (`id`)"); + database.execSQL("CREATE INDEX `index_Board_ownerId` ON `" + tableName + "` (`ownerId`)"); + database.execSQL("CREATE INDEX `index_Board_lastModifiedLocal` ON `" + tableName + "` (`lastModifiedLocal`)"); + database.execSQL("UPDATE SQLITE_SEQUENCE SET seq = (select max(id) from " + tableName + ") WHERE name = ?", new Object[]{tableName}); + } + { + String tableName = "Label"; + database.execSQL("CREATE TABLE `" + tableName + suffix + "` (`localId` INTEGER PRIMARY KEY AUTOINCREMENT, `accountId` INTEGER NOT NULL, `id` INTEGER, `status` INTEGER NOT NULL, " + + "`lastModified` INTEGER, `lastModifiedLocal` INTEGER, `title` TEXT, `color` INTEGER NOT NULL DEFAULT 0, `boardId` INTEGER NOT NULL, " + + "FOREIGN KEY(`boardId`) REFERENCES `Board`(`localId`) ON UPDATE NO ACTION ON DELETE CASCADE )"); + Cursor cursor = database.query("select * from `" + tableName + "`"); + while (cursor.moveToNext()) { + String colorAsString1 = cursor.getString(7); // color + + @ColorInt Integer color1 = null; + try { + color1 = Color.parseColor(ColorUtil.INSTANCE.formatColorToParsableHexString(colorAsString1)); + } catch (Exception e) { + color1 = Color.GRAY; + } + database.execSQL("Insert into `" + tableName + suffix + "` VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", new Object[]{ + cursor.getLong(0), cursor.getLong(1), cursor.getLong(2), cursor.getInt(3), + cursor.getLong(4), cursor.getLong(5), cursor.getString(6), color1, cursor.getLong(8)}); + + } + + + database.execSQL("DROP TABLE `" + tableName + "`"); + database.execSQL("ALTER TABLE `" + tableName + suffix + "` RENAME TO `" + tableName + "`"); + database.execSQL("CREATE UNIQUE INDEX `index_Label_accountId_id` ON `" + tableName + "` (`accountId`, `id`)"); + database.execSQL("CREATE INDEX `index_Label_boardId` ON `" + tableName + "` (`boardId`)"); + database.execSQL("CREATE INDEX `index_Label_accountId` ON `" + tableName + "` (`accountId`)"); + database.execSQL("CREATE UNIQUE INDEX `idx_label_title_unique` ON `" + tableName + "` (`boardId`, `title`)"); + database.execSQL("CREATE INDEX `index_Label_id` ON `" + tableName + "` (`id`)"); + database.execSQL("CREATE INDEX `index_Label_lastModifiedLocal` ON `" + tableName + "` (`lastModifiedLocal`)"); + database.execSQL("UPDATE SQLITE_SEQUENCE SET seq = (select max(id) from " + tableName + ") WHERE name = ?", new Object[]{tableName}); + } + } + }; + public static final RoomDatabase.Callback ON_CREATE_CALLBACK = new RoomDatabase.Callback() { @Override public void onCreate(@NonNull SupportSQLiteDatabase db) { super.onCreate(db); @@ -216,6 +414,12 @@ public abstract class DeckDatabase extends RoomDatabase { .apply(); } }) + .addMigrations(MIGRATION_15_16) + .addMigrations(MIGRATION_16_17) + .addMigrations(MIGRATION_17_18) + .addMigrations(MIGRATION_18_19) + .addMigrations(MIGRATION_19_20) + .addMigrations(MIGRATION_20_21) .fallbackToDestructiveMigration() .addCallback(ON_CREATE_CALLBACK) .build(); @@ -256,4 +460,16 @@ public abstract class DeckDatabase extends RoomDatabase { public abstract MentionDao getMentionDao(); public abstract SingleCardWidgetModelDao getSingleCardWidgetModelDao(); + + public abstract StackWidgetModelDao getStackWidgetModelDao(); + + public abstract OcsProjectDao getOcsProjectDao(); + + public abstract OcsProjectResourceDao getOcsProjectResourceDao(); + + public abstract JoinCardWithOcsProjectDao getJoinCardWithOcsProjectDao(); + + public abstract UserInGroupDao getUserInGroupDao(); + + public abstract UserInBoardDao getUserInBoardDao(); }
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/AttachmentDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/AttachmentDao.java index 2d0887903..8f7ee1cba 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/AttachmentDao.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/AttachmentDao.java @@ -25,6 +25,10 @@ public interface AttachmentDao extends GenericDao<Attachment> { @Query("SELECT * FROM attachment WHERE accountId = :accountId and (status<>1 or id is null or lastModified <> lastModifiedLocal)") List<Attachment> getLocallyChangedAttachmentsDirectly(long accountId); + @Query("SELECT a.* FROM attachment a inner join card c on c.localId = a.cardId " + + "WHERE c.stackId = :localStackId and (a.status<>1 or a.id is null or a.lastModified <> a.lastModifiedLocal)") + List<Attachment> getLocallyChangedAttachmentsForStackDirectly(long localStackId); + @Query("SELECT * FROM attachment WHERE accountId = :accountId and cardId = :localCardId") List<Attachment> getAttachmentsForLocalCardIdDirectly(long accountId, Long localCardId); }
\ No newline at end of file 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..6cb322aa3 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 @@ -13,6 +13,9 @@ import it.niedermann.nextcloud.deck.model.full.FullBoard; @Dao public interface BoardDao extends GenericDao<Board> { + @Query("SELECT * FROM board WHERE accountId = :accountId and (deletedAt = 0 or deletedAt is null) and status <> 3 order by title asc") + LiveData<List<Board>> getBoardsForAccount(final long accountId); + @Query("SELECT * FROM board WHERE accountId = :accountId and archived = 1 and (deletedAt = 0 or deletedAt is null) and status <> 3 order by title asc") LiveData<List<Board>> getArchivedBoardsForAccount(final long accountId); @@ -52,7 +55,8 @@ 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") + @Transaction + @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 +76,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/dao/CardDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/CardDao.java index 57163112a..82bdf1b8d 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/CardDao.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/CardDao.java @@ -11,6 +11,7 @@ import java.util.List; import it.niedermann.nextcloud.deck.model.Card; import it.niedermann.nextcloud.deck.model.full.FullCard; +import it.niedermann.nextcloud.deck.model.full.FullCardWithProjects; @Dao public interface CardDao extends GenericDao<Card> { @@ -36,17 +37,24 @@ public interface CardDao extends GenericDao<Card> { @Query("SELECT * FROM card WHERE accountId = :accountId AND archived = 0 AND stackId = :localStackId and status<>3 order by `order`, createdAt asc") LiveData<List<FullCard>> getFullCardsForStack(final long accountId, final long localStackId); - @Transaction // v not deleted! + @Transaction @RawQuery(observedEntities = Card.class) LiveData<List<FullCard>> getFilteredFullCardsForStack(SupportSQLiteQuery query); @Transaction + @RawQuery(observedEntities = Card.class) + List<FullCard> getFilteredFullCardsForStackDirectly(SupportSQLiteQuery query); + + @Transaction @Query("SELECT * FROM card WHERE accountId = :accountId AND stackId = :localStackId order by `order`, createdAt asc") List<FullCard> getFullCardsForStackDirectly(final long accountId, final long localStackId); @Transaction @Query("SELECT * FROM card WHERE accountId = :accountId and localId = :localCardId") LiveData<FullCard> getFullCardByLocalId(final long accountId, final long localCardId); + @Transaction + @Query("SELECT * FROM card WHERE accountId = :accountId and localId = :localCardId") + LiveData<FullCardWithProjects> getFullCardWithProjectsByLocalId(final long accountId, final long localCardId); @Transaction @Query("SELECT * FROM card WHERE accountId = :accountId and id = :remoteId") @@ -66,9 +74,15 @@ public interface CardDao extends GenericDao<Card> { @Query("SELECT * FROM card c WHERE accountId = :accountId and exists ( select 1 from DeckComment dc where dc.objectId = c.localId and dc.status<>1)") List<Card> getCardsWithLocallyChangedCommentsDirectly(Long accountId); + @Query("SELECT * FROM card c WHERE stackId = :localStackId and exists ( select 1 from DeckComment dc where dc.objectId = c.localId and dc.status<>1)") + List<Card> getCardsWithLocallyChangedCommentsForStackDirectly(Long localStackId); + @Query("SELECT count(*) FROM card c WHERE accountId = :accountId and stackId = :localStackId and status <> 3") LiveData<Integer> countCardsInStack(long accountId, long localStackId); @Query("SELECT coalesce(MAX(`order`), -1) FROM card c WHERE stackId = :localStackId and status <> 3") Integer getHighestOrderInStack(Long localStackId); + + @Query("SELECT c.stackId FROM card c WHERE localId = :localCardId") + Long getLocalStackIdByLocalCardId(Long localCardId); }
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/JoinCardWithLabelDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/JoinCardWithLabelDao.java index c96c64e7d..26e6c65a8 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/JoinCardWithLabelDao.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/JoinCardWithLabelDao.java @@ -40,6 +40,9 @@ public interface JoinCardWithLabelDao extends GenericDao<JoinCardWithLabel> { @Query("select * from joincardwithlabel WHERE status <> 1") // not UP_TO_DATE List<JoinCardWithLabel> getAllChangedJoins(); + @Query("select j.* from joincardwithlabel j inner join card c on j.cardId = c.localId WHERE c.stackId = :localStackId and j.status <> 1") // not UP_TO_DATE + List<JoinCardWithLabel> getAllChangedJoinsForStack(Long localStackId); + @Query("delete from joincardwithlabel " + "where cardId = (select c.localId from card c where c.accountId = :accountId and c.id = :remoteCardId) " + "and labelId = (select l.localId from label l where l.accountId = :accountId and l.id = :remoteLabelId)") diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/JoinCardWithUserDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/JoinCardWithUserDao.java index 46554685f..416d52eed 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/JoinCardWithUserDao.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/JoinCardWithUserDao.java @@ -25,7 +25,14 @@ public interface JoinCardWithUserDao extends GenericDao<JoinCardWithUser> { "inner join card c on j.cardId = c.localId " + "inner join user u on j.userId = u.localId " + "WHERE j.status <> 1") // not UP_TO_DATE - List<JoinCardWithUser> getDeletedJoinsWithRemoteIDs(); + List<JoinCardWithUser> getChangedJoinsWithRemoteIDs(); + + @Query("select u.localId as userId, c.id as cardId, j.status from joincardwithuser j " + + "inner join card c on j.cardId = c.localId " + + "inner join user u on j.userId = u.localId " + + "WHERE c.stackId = :localStackId " + + "AND j.status <> 1") // not UP_TO_DATE + List<JoinCardWithUser> getChangedJoinsWithRemoteIDsForStack(Long localStackId); @Query("delete from joincardwithuser " + "where cardId = (select c.localId from card c where c.accountId = :accountId and c.id = :remoteCardId) " + diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/StackDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/StackDao.java index 0fbccbe08..feb7e453b 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/StackDao.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/StackDao.java @@ -13,7 +13,7 @@ import it.niedermann.nextcloud.deck.model.full.FullStack; @Dao public interface StackDao extends GenericDao<Stack> { - @Query("SELECT * FROM stack WHERE accountId = :accountId AND boardId = :localBoardId order by `order` asc") + @Query("SELECT * FROM stack WHERE accountId = :accountId AND boardId = :localBoardId and status<>3 and (deletedAt is null or deletedAt = 0) order by `order` asc") LiveData<List<Stack>> getStacksForBoard(final long accountId, final long localBoardId); @Query("SELECT * FROM stack WHERE accountId = :accountId and boardId = :localBoardId and id = :remoteId") @@ -31,10 +31,6 @@ public interface StackDao extends GenericDao<Stack> { FullStack getFullStackByRemoteIdDirectly(final long accountId, final long localBoardId, final long remoteId); @Transaction - @Query("SELECT * FROM stack WHERE accountId = :accountId AND boardId = :localBoardId and status<>3 and (deletedAt is null or deletedAt = 0) order by `order` asc") - LiveData<List<FullStack>> getFullStacksForBoard(final long accountId, final long localBoardId); - - @Transaction @Query("SELECT * FROM stack WHERE accountId = :accountId and boardId = :localBoardId and id = :remoteId") LiveData<FullStack> getFullStackByRemoteId(final long accountId, final long localBoardId, final long remoteId); @@ -56,4 +52,7 @@ public interface StackDao extends GenericDao<Stack> { @Query("SELECT localId FROM stack s WHERE accountId = :accountId and id = :stackId") Long getLocalStackIdByRemoteStackIdDirectly(long accountId, Long stackId); + + @Query("SELECT coalesce(MAX(`order`), -1) FROM stack s WHERE boardId = :localBoardId") + Integer getHighestStackOrderInBoard(long localBoardId); }
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/UserDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/UserDao.java index 1d14a276a..6e42cdaae 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/UserDao.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/UserDao.java @@ -26,12 +26,16 @@ public interface UserDao extends GenericDao<User> { " where ju.userId = u.localId" + " and ju.cardId = :notYetAssignedToLocalCardId AND status <> 3" + // not LOCAL_DELETED " )" + - " AND" + - " (" + - " EXISTS (" + - " select 1 from accesscontrol" + - " where userId = u.localId and boardId = :boardId" + - " )" + + " AND ( " + + " EXISTS (" + + " select 1 from userinboard where boardId = :boardId AND userId = u.localId" + + " )" + + " OR" + + " EXISTS (" + + " select 1 from accesscontrol" + // v GROUP! + " where (userId = u.localId OR (type = 1 and exists(select 1 from UserInGroup uig where uig.memberId = u.localId and uig.groupId = userId))) " + + " and boardId = :boardId and status <> 3" + + " )" + " OR" + " EXISTS (" + " select 1 from board where localId = :boardId AND ownerId = u.localId" + @@ -48,16 +52,6 @@ public interface UserDao extends GenericDao<User> { "and ( uid LIKE :searchTerm or displayname LIKE :searchTerm or primaryKey LIKE :searchTerm ) " + "and u.localId <> (select b.ownerId from board b where localId = :boardId)" + "ORDER BY u.displayname") - LiveData<List<User>> searchUserByUidOrDisplayNameForACL(final long accountId, final long boardId, final String searchTerm); - - @Query("SELECT u.* FROM user u WHERE accountId = :accountId " + - " AND NOT EXISTS (" + - " select 1 from accesscontrol ju" + - " where ju.userId = u.localId and ju.boardId = :boardId and status <> 3" + // not LOCAL_DELETED - " ) " + - "and ( uid LIKE :searchTerm or displayname LIKE :searchTerm or primaryKey LIKE :searchTerm ) " + - "and u.localId <> (select b.ownerId from board b where localId = :boardId)" + - "ORDER BY u.displayname") List<User> searchUserByUidOrDisplayNameForACLDirectly(final long accountId, final long boardId, final String searchTerm); @Query("SELECT * FROM user WHERE accountId = :accountId and uid = :uid") @@ -76,12 +70,16 @@ public interface UserDao extends GenericDao<User> { " where ju.userId = u.localId" + " and ju.cardId = :notAssignedToLocalCardId AND status <> 3" + // not LOCAL_DELETED " )" + - " AND" + - " (" + - " EXISTS (" + - " select 1 from accesscontrol" + - " where userId = u.localId and boardId = :boardId" + - " )" + + " AND ( " + + " EXISTS (" + + " select 1 from userinboard where boardId = :boardId AND userId = u.localId" + + " )" + + " OR" + + " EXISTS (" + + " select 1 from accesscontrol" + // v GROUP! + " where (userId = u.localId OR (type = 1 and exists(select 1 from UserInGroup uig where uig.memberId = u.localId and uig.groupId = userId))) " + + " and boardId = :boardId and status <> 3" + + " )" + " OR" + " EXISTS (" + " select 1 from board where localId = :boardId AND ownerId = u.localId" + diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/UserInBoardDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/UserInBoardDao.java new file mode 100644 index 000000000..7a0476c53 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/UserInBoardDao.java @@ -0,0 +1,12 @@ +package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao; + +import androidx.room.Dao; +import androidx.room.Query; + +import it.niedermann.nextcloud.deck.model.relations.UserInBoard; + +@Dao +public interface UserInBoardDao extends GenericDao<UserInBoard> { + @Query("DELETE FROM userinboard WHERE boardId = :localId") + void deleteByBoardId(long localId); +}
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/UserInGroupDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/UserInGroupDao.java new file mode 100644 index 000000000..fb3f5b26e --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/UserInGroupDao.java @@ -0,0 +1,12 @@ +package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao; + +import androidx.room.Dao; +import androidx.room.Query; + +import it.niedermann.nextcloud.deck.model.relations.UserInGroup; + +@Dao +public interface UserInGroupDao extends GenericDao<UserInGroup> { + @Query("DELETE FROM useringroup WHERE groupId = :localId") + void deleteByGroupId(long localId); +}
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/projects/JoinCardWithOcsProjectDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/projects/JoinCardWithOcsProjectDao.java new file mode 100644 index 000000000..d132796e4 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/projects/JoinCardWithOcsProjectDao.java @@ -0,0 +1,13 @@ +package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.projects; + +import androidx.room.Dao; +import androidx.room.Query; + +import it.niedermann.nextcloud.deck.model.ocs.projects.JoinCardWithProject; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.GenericDao; + +@Dao +public interface JoinCardWithOcsProjectDao extends GenericDao<JoinCardWithProject> { + @Query("select * from JoinCardWithProject where projectId = :localProjectId and cardId = :localCardId") + JoinCardWithProject getAssignmentByCardIdAndProjectIdDirectly(Long localCardId, Long localProjectId); +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/projects/OcsProjectDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/projects/OcsProjectDao.java new file mode 100644 index 000000000..fb0e3f836 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/projects/OcsProjectDao.java @@ -0,0 +1,13 @@ +package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.projects; + +import androidx.room.Dao; +import androidx.room.Query; + +import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProject; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.GenericDao; + +@Dao +public interface OcsProjectDao extends GenericDao<OcsProject> { + @Query("select * from OcsProject where accountId = :accountId and id = :remoteId") + OcsProject getProjectByRemoteIdDirectly(long accountId, Long remoteId); +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/projects/OcsProjectResourceDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/projects/OcsProjectResourceDao.java new file mode 100644 index 000000000..8a544667a --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/projects/OcsProjectResourceDao.java @@ -0,0 +1,25 @@ +package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.projects; + +import androidx.lifecycle.LiveData; +import androidx.room.Dao; +import androidx.room.Query; + +import java.util.List; + +import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProjectResource; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.GenericDao; + +@Dao +public interface OcsProjectResourceDao extends GenericDao<OcsProjectResource> { + @Query("delete from OcsProjectResource where projectId = :localProjectId") + void deleteByProjectId(Long localProjectId); + + @Query("select * from OcsProjectResource where projectId = :localProjectId") + LiveData<List<OcsProjectResource>> getResourcesByLocalProjectId(Long localProjectId); + + @Query("select count(id) from OcsProjectResource where projectId = :localProjectId") + int countProjectResourcesInProjectDirectly(Long localProjectId); + + @Query("select count(id) from OcsProjectResource where projectId = :localProjectId") + LiveData<Integer> countProjectResourcesInProject(Long localProjectId); +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/SingleCardWidgetModelDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/widgets/SingleCardWidgetModelDao.java index 0c2a485c1..a26269c30 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/SingleCardWidgetModelDao.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/widgets/SingleCardWidgetModelDao.java @@ -1,4 +1,4 @@ -package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao; +package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.widgets; import androidx.room.Dao; import androidx.room.Query; @@ -6,6 +6,7 @@ import androidx.room.Transaction; import it.niedermann.nextcloud.deck.model.full.FullSingleCardWidgetModel; import it.niedermann.nextcloud.deck.model.widget.singlecard.SingleCardWidgetModel; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.GenericDao; @Dao public interface SingleCardWidgetModelDao extends GenericDao<SingleCardWidgetModel> { diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/widgets/StackWidgetModelDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/widgets/StackWidgetModelDao.java new file mode 100644 index 000000000..9b370e32b --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/adapters/db/dao/widgets/StackWidgetModelDao.java @@ -0,0 +1,19 @@ +package it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.widgets; + +import androidx.room.Dao; +import androidx.room.Query; +import androidx.room.Transaction; + +import it.niedermann.nextcloud.deck.model.appwidgets.StackWidgetModel; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.dao.GenericDao; + +@Dao +public interface StackWidgetModelDao extends GenericDao<StackWidgetModel> { + + @Query("SELECT * FROM stackwidgetmodel WHERE appwidgetid = :appWidgetId") + StackWidgetModel getStackWidgetByAppWidgetIdDirectly(final int appWidgetId); + + @Transaction + @Query("SELECT EXISTS (SELECT 1 FROM stackwidgetmodel WHERE stackId in (:stackLocalIds))") + boolean containsStackLocalId(final long... stackLocalIds); +} 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..6f7c0d9c0 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 @@ -31,12 +31,17 @@ public class SyncHelper { // Sync Server -> App public <T extends IRemoteEntity> void doSyncFor(final AbstractSyncDataProvider<T> provider){ provider.registerChildInParent(provider); - provider.getAllFromServer(serverAdapter, accountId, new IResponseCallback<List<T>>(account) { + provider.getAllFromServer(serverAdapter, dataBaseAdapter, accountId, new IResponseCallback<List<T>>(account) { @Override public void onResponse(List<T> response) { 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); @@ -126,6 +131,7 @@ public class SyncHelper { public void onResponse(T response) { response.setAccountId(this.account.getId()); T update = applyUpdatesFromRemote(provider, entity, response, accountId); + update.setId(response.getId()); update.setStatus(DBStatus.UP_TO_DATE.getId()); provider.updateInDB(dataBaseAdapter, accountId, update, false); provider.goDeeperForUpSync(SyncHelper.this, serverAdapter, dataBaseAdapter, responseCallback); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/AbstractSyncDataProvider.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/AbstractSyncDataProvider.java index 166d6c519..42b034026 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/AbstractSyncDataProvider.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/AbstractSyncDataProvider.java @@ -4,6 +4,7 @@ import java.util.ArrayList; 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.model.interfaces.IRemoteEntity; import it.niedermann.nextcloud.deck.persistence.sync.adapters.ServerAdapter; @@ -39,9 +40,17 @@ public abstract class AbstractSyncDataProvider<T extends IRemoteEntity> { public static <T extends IRemoteEntity> List<T> findDelta(List<T> listA, List<T> listB){ List<T> delta = new ArrayList<>(); for (T b : listB) { + if (b == null) { + DeckLog.error("Entry in listB is null! skipping..."); + continue; + } boolean found = false; for (T a : listA) { - if ((a.getLocalId()!= null && b.getLocalId()!= null ? (a.getLocalId().equals(b.getLocalId())) + if (a == null) { + DeckLog.error("Entry in listA is null! skipping..."); + continue; + } + if ((a.getLocalId() != null && b.getLocalId() != null ? (a.getLocalId().equals(b.getLocalId())) : a.getId().equals(b.getId())) && b.getAccountId() == a.getAccountId()) { found = true; break; @@ -58,7 +67,13 @@ public abstract class AbstractSyncDataProvider<T extends IRemoteEntity> { children.add(child); } - public abstract void getAllFromServer(ServerAdapter serverAdapter, long accountId, IResponseCallback<List<T>> responder, Date lastSync); + public void getAllFromServer(ServerAdapter serverAdapter, long accountId, IResponseCallback<List<T>> responder, Date lastSync) { + return; + } + public void getAllFromServer(ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, long accountId, IResponseCallback<List<T>> responder, Date lastSync) { + // Overridden, because we also need the DB-Adapter at some points here (see ACL data provider) + getAllFromServer(serverAdapter, accountId, responder, lastSync); + } public abstract T getSingleFromDB(DataBaseAdapter dataBaseAdapter, long accountId, T entity); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/AccessControlDataProvider.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/AccessControlDataProvider.java index 3242ab7c1..aab3aeb09 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/AccessControlDataProvider.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/AccessControlDataProvider.java @@ -2,16 +2,23 @@ package it.niedermann.nextcloud.deck.persistence.sync.helpers.providers; import java.util.Date; import java.util.List; +import java.util.concurrent.CountDownLatch; +import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.api.IResponseCallback; import it.niedermann.nextcloud.deck.model.AccessControl; +import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.model.User; import it.niedermann.nextcloud.deck.model.full.FullBoard; +import it.niedermann.nextcloud.deck.model.ocs.user.GroupMemberUIDs; +import it.niedermann.nextcloud.deck.model.ocs.user.OcsUser; 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.util.AsyncUtil; public class AccessControlDataProvider extends AbstractSyncDataProvider<AccessControl> { + private static final Long TYPE_GROUP = 1L; private List<AccessControl> acl; private FullBoard board; @@ -22,10 +29,66 @@ public class AccessControlDataProvider extends AbstractSyncDataProvider<AccessCo } @Override - public void getAllFromServer(ServerAdapter serverAdapter, long accountId, IResponseCallback<List<AccessControl>> responder, Date lastSync) { + public void getAllFromServer(ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, long accountId, IResponseCallback<List<AccessControl>> responder, Date lastSync) { + AsyncUtil.awaitAsyncWork(acl.size(), latch -> { + for (AccessControl accessControl : acl) { + if (accessControl.getType() == TYPE_GROUP) { + serverAdapter.searchGroupMembers(accessControl.getUser().getUid(), new IResponseCallback<GroupMemberUIDs>(responder.getAccount()) { + @Override + public void onResponse(GroupMemberUIDs response) { + accessControl.setGroupMemberUIDs(response); + if (response.getUids().size() > 0) { + ensureGroupMembersInDB(getAccount(), dataBaseAdapter, serverAdapter, response); + } + latch.countDown(); + } + + @Override + public void onError(Throwable throwable) { + super.onError(throwable); + latch.countDown(); + } + }); + } else latch.countDown(); + } + }); + responder.onResponse(acl); } + private void ensureGroupMembersInDB(Account account, DataBaseAdapter dataBaseAdapter, ServerAdapter serverAdapter, GroupMemberUIDs response) { + CountDownLatch memberLatch = new CountDownLatch(response.getUids().size()); + for (String uid : response.getUids()) { + User user = dataBaseAdapter.getUserByUidDirectly(account.getId(), uid); + if (user == null) { + // unknown user. fetch! + serverAdapter.getSingleUserData(uid, new IResponseCallback<OcsUser>(account) { + @Override + public void onResponse(OcsUser response) { + DeckLog.log(response.toString()); + User user = new User(); + user.setUid(response.getId()); + user.setPrimaryKey(response.getId()); + user.setDisplayname(response.getDisplayName()); + dataBaseAdapter.createUser(account.getId(), user); + memberLatch.countDown(); + } + + @Override + public void onError(Throwable throwable) { + super.onError(throwable); + memberLatch.countDown(); + } + }); + } else memberLatch.countDown(); + } + try { + memberLatch.await(); + } catch (InterruptedException e) { + DeckLog.logError(e); + } + } + @Override public AccessControl getSingleFromDB(DataBaseAdapter dataBaseAdapter, long accountId, AccessControl entity) { return dataBaseAdapter.getAccessControlByRemoteIdDirectly(accountId, entity.getEntity().getId()); @@ -34,7 +97,26 @@ public class AccessControlDataProvider extends AbstractSyncDataProvider<AccessCo @Override public long createInDB(DataBaseAdapter dataBaseAdapter, long accountId, AccessControl entity) { prepareUser(dataBaseAdapter, accountId, entity); - return dataBaseAdapter.createAccessControl(accountId, entity); + long newId = dataBaseAdapter.createAccessControl(accountId, entity); + entity.setLocalId(newId); + handleGroupMemberships(dataBaseAdapter, entity); + return newId; + } + + private void handleGroupMemberships(DataBaseAdapter dataBaseAdapter, AccessControl entity) { + if (entity.getType() != TYPE_GROUP) { + return; + } + dataBaseAdapter.deleteGroupMembershipsOfGroup(entity.getUser().getLocalId()); + if (entity.getGroupMemberUIDs() == null) { + return; + } + for (String groupMemberUID : entity.getGroupMemberUIDs().getUids()) { + User member = dataBaseAdapter.getUserByUidDirectly(entity.getAccountId(), groupMemberUID); + if (member != null) { + dataBaseAdapter.addUserToGroup(entity.getUserId(), member.getLocalId()); + } + } } private void prepareUser(DataBaseAdapter dataBaseAdapter, long accountId, AccessControl entity) { @@ -42,6 +124,7 @@ public class AccessControlDataProvider extends AbstractSyncDataProvider<AccessCo if (user == null) { long userId = dataBaseAdapter.createUser(accountId, entity.getUser()); entity.setUserId(userId); + entity.getUser().setLocalId(userId); } else { entity.setUserId(user.getLocalId()); entity.getUser().setLocalId(user.getLocalId()); @@ -52,7 +135,9 @@ public class AccessControlDataProvider extends AbstractSyncDataProvider<AccessCo @Override public void updateInDB(DataBaseAdapter dataBaseAdapter, long accountId, AccessControl entity, boolean setStatus) { prepareUser(dataBaseAdapter, accountId, entity); + entity.setBoardId(board.getLocalId()); dataBaseAdapter.updateAccessControl(entity, setStatus); + handleGroupMemberships(dataBaseAdapter, entity); } @Override @@ -64,6 +149,9 @@ public class AccessControlDataProvider extends AbstractSyncDataProvider<AccessCo public void createOnServer(ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, long accountId, IResponseCallback<AccessControl> responder, AccessControl entity) { AccessControl acl = new AccessControl(entity); acl.setBoardId(board.getBoard().getId()); + if (acl.getUser() == null && acl.getUserId() != null) { + acl.setUser(dataBaseAdapter.getUserByLocalIdDirectly(acl.getUserId())); + } serverAdapter.createAccessControl(board.getBoard().getId(), acl, responder); } @@ -79,6 +167,7 @@ public class AccessControlDataProvider extends AbstractSyncDataProvider<AccessCo @Override public void deletePhysicallyInDB(DataBaseAdapter dataBaseAdapter, long accountId, AccessControl accessControl) { + dataBaseAdapter.deleteGroupMembershipsOfGroup(accessControl.getUser().getLocalId()); dataBaseAdapter.deleteAccessControl(accessControl, false); } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/ActivityDataProvider.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/ActivityDataProvider.java index a3785ca78..bf79684f0 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/ActivityDataProvider.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/ActivityDataProvider.java @@ -1,6 +1,8 @@ package it.niedermann.nextcloud.deck.persistence.sync.helpers.providers; -import java.util.ArrayList; +import androidx.annotation.NonNull; + +import java.util.Collections; import java.util.Date; import java.util.List; @@ -12,9 +14,10 @@ import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.DataBaseAdapter public class ActivityDataProvider extends AbstractSyncDataProvider<Activity> { - protected Card card; + @NonNull + private final Card card; - public ActivityDataProvider(AbstractSyncDataProvider<?> parent, Card card) { + public ActivityDataProvider(AbstractSyncDataProvider<?> parent, @NonNull Card card) { super(parent); this.card = card; } @@ -66,6 +69,6 @@ public class ActivityDataProvider extends AbstractSyncDataProvider<Activity> { @Override public List<Activity> getAllChangedFromDB(DataBaseAdapter dataBaseAdapter, long accountId, Date lastSync) { - return new ArrayList<>(); + return Collections.emptyList(); } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/BoardDataProvider.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/BoardDataProvider.java index 98163029f..45ea6cff6 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/BoardDataProvider.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/BoardDataProvider.java @@ -6,9 +6,7 @@ import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.concurrent.CountDownLatch; -import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.api.IResponseCallback; import it.niedermann.nextcloud.deck.model.AccessControl; import it.niedermann.nextcloud.deck.model.Board; @@ -19,6 +17,7 @@ import it.niedermann.nextcloud.deck.model.full.FullStack; 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.util.AsyncUtil; public class BoardDataProvider extends AbstractSyncDataProvider<FullBoard> { @@ -39,27 +38,65 @@ public class BoardDataProvider extends AbstractSyncDataProvider<FullBoard> { @Override public long createInDB(DataBaseAdapter dataBaseAdapter, long accountId, FullBoard entity) { handleOwner(dataBaseAdapter, accountId, entity); - return dataBaseAdapter.createBoardDirectly(accountId, entity.getBoard()); + Long localId = dataBaseAdapter.createBoardDirectly(accountId, entity.getBoard()); + entity.getBoard().setLocalId(localId); + handleUsers(dataBaseAdapter, accountId, entity); + return localId; } private void handleOwner(DataBaseAdapter dataBaseAdapter, long accountId, FullBoard entity) { if (entity.getOwner()!=null) { - User remoteOwner = entity.getOwner(); - User owner = dataBaseAdapter.getUserByUidDirectly(accountId, remoteOwner.getUid()); - if (owner == null){ - dataBaseAdapter.createUser(accountId, remoteOwner); - } else { - dataBaseAdapter.updateUser(accountId, remoteOwner, false); - } - owner = dataBaseAdapter.getUserByUidDirectly(accountId, remoteOwner.getUid()); + User owner = createOrUpdateUser(dataBaseAdapter,accountId, entity.getOwner()); entity.getBoard().setOwnerId(owner.getLocalId()); } } + private void handleUsers(DataBaseAdapter dataBaseAdapter, long accountId, FullBoard entity) { + dataBaseAdapter.deleteBoardMembershipsOfBoard(entity.getLocalId()); + if (entity.getUsers()!=null && !entity.getUsers().isEmpty()) { + for (User user : entity.getUsers()) { + if (user == null) { + continue; + } + User existing = createOrUpdateUser(dataBaseAdapter, accountId, user); + dataBaseAdapter.addUserToBoard(existing.getLocalId(), entity.getLocalId()); + } + } + } + + private User createOrUpdateUser(DataBaseAdapter dataBaseAdapter, long accountId, User remoteUser) { + User owner = dataBaseAdapter.getUserByUidDirectly(accountId, remoteUser.getUid()); + if (owner == null){ + dataBaseAdapter.createUser(accountId, remoteUser); + } else { + dataBaseAdapter.updateUser(accountId, remoteUser, false); + } + return dataBaseAdapter.getUserByUidDirectly(accountId, remoteUser.getUid()); + } + @Override public void updateInDB(DataBaseAdapter dataBaseAdapter, long accountId, FullBoard entity, boolean setStatus) { + handleDefaultLabels(dataBaseAdapter, entity); handleOwner(dataBaseAdapter, accountId, entity); dataBaseAdapter.updateBoard(entity.getBoard(), setStatus); + handleUsers(dataBaseAdapter, accountId, entity); + } + + private void handleDefaultLabels(DataBaseAdapter dataBaseAdapter, FullBoard entity) { + // ## merge labels (created at board creation): + // the server creates four default labels. if a board is copied, they will also be copied. At sync, after creating the board, the labels are already there. + // this merges the created default ones with the ones i already have. + if (entity!= null && entity.getLabels() != null) { + for (Label label : entity.getLabels()) { + // does this label exist and unknown to server yet? + Label existing = dataBaseAdapter.getLabelByBoardIdAndTitleDirectly(entity.getLocalId(), label.getTitle()); + if (existing != null && existing.getId() == null) { + // take our label and lets say it IS the same as on server (but use the local color, no matter what the server says) + existing.setId(label.getId()); + dataBaseAdapter.updateLabel(existing, false); + } + } + } } @Override @@ -102,17 +139,13 @@ public class BoardDataProvider extends AbstractSyncDataProvider<FullBoard> { public void goDeeperForUpSync(SyncHelper syncHelper, ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, IResponseCallback<Boolean> callback) { Long accountId = callback.getAccount().getId(); List<Label> locallyChangedLabels = dataBaseAdapter.getLocallyChangedLabels(accountId); - CountDownLatch countDownLatch = new CountDownLatch(locallyChangedLabels.size()); - for (Label label : locallyChangedLabels) { - Board board = dataBaseAdapter.getBoardByLocalIdDirectly(label.getBoardId()); - label.setBoardId(board.getId()); - syncHelper.doUpSyncFor(new LabelDataProvider(this, board, Collections.singletonList(label)), countDownLatch); - } - try { - countDownLatch.await(); - } catch (InterruptedException e) { - DeckLog.logError(e); - } + AsyncUtil.awaitAsyncWork(locallyChangedLabels.size(), (countDownLatch) -> { + for (Label label : locallyChangedLabels) { + Board board = dataBaseAdapter.getBoardByLocalIdDirectly(label.getBoardId()); + label.setBoardId(board.getId()); + syncHelper.doUpSyncFor(new LabelDataProvider(this, board, Collections.singletonList(label)), countDownLatch); + } + }); List<Long> localBoardIDsWithChangedACL = dataBaseAdapter.getBoardIDsOfLocallyChangedAccessControl(accountId); for (Long boardId : localBoardIDsWithChangedACL) { diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/CardDataProvider.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/CardDataProvider.java index 03e02bd70..376948d54 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/CardDataProvider.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/CardDataProvider.java @@ -70,7 +70,7 @@ public class CardDataProvider extends AbstractSyncDataProvider<FullCard> { @Override public long createInDB(DataBaseAdapter dataBaseAdapter, long accountId, FullCard entity) { fixRelations(dataBaseAdapter, accountId, entity); - return dataBaseAdapter.createCard(accountId, entity.getCard()); + return dataBaseAdapter.createCardDirectly(accountId, entity.getCard()); } protected CardUpdate toCardUpdate(FullCard card) { @@ -144,12 +144,15 @@ public class CardDataProvider extends AbstractSyncDataProvider<FullCard> { } else { DeckLog.verbose("Comments - Version is too low, DONT SYNC"); } + syncHelper.doSyncFor(new OcsProjectDataProvider(this, existingEntity.getCard())); } @Override public void createOnServer(ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, long accountId, IResponseCallback<FullCard> responder, FullCard entity) { entity.getCard().setStackId(stack.getId()); - serverAdapter.createCard(board.getId(), stack.getId(), entity.getCard(), responder); +// if (board != null && stack != null && board.getId() != null && stack.getId() != null) { + serverAdapter.createCard(board.getId(), stack.getId(), entity.getCard(), responder); +// } else DeckLog.error("Skipped card creation due to missing remote ID"); } @Override @@ -184,8 +187,13 @@ public class CardDataProvider extends AbstractSyncDataProvider<FullCard> { public void goDeeperForUpSync(SyncHelper syncHelper, ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, IResponseCallback<Boolean> callback) { FullStack stack; Board board; + List<JoinCardWithLabel> changedLabels; + if (this.stack == null) { + changedLabels = dataBaseAdapter.getAllChangedLabelJoins(); + } else { + changedLabels = dataBaseAdapter.getAllChangedLabelJoinsForStack(this.stack.getLocalId()); + } - List<JoinCardWithLabel> changedLabels = dataBaseAdapter.getAllChangedJoins(); Account account = callback.getAccount(); for (JoinCardWithLabel changedLabelLocal : changedLabels) { Card card = dataBaseAdapter.getCardByLocalIdDirectly(account.getId(), changedLabelLocal.getCardId()); @@ -201,7 +209,7 @@ public class CardDataProvider extends AbstractSyncDataProvider<FullCard> { board = this.board; } - JoinCardWithLabel changedLabel = dataBaseAdapter.getRemoteIdsForJoin(changedLabelLocal.getCardId(), changedLabelLocal.getLabelId()); + JoinCardWithLabel changedLabel = dataBaseAdapter.getAllChangedLabelJoinsWithRemoteIDs(changedLabelLocal.getCardId(), changedLabelLocal.getLabelId()); if (changedLabel.getStatusEnum() == DBStatus.LOCAL_DELETED) { if (changedLabel.getLabelId() == null || changedLabel.getCardId() == null) { dataBaseAdapter.deleteJoinedLabelForCardPhysicallyByRemoteIDs(account.getId(), changedLabel.getCardId(), changedLabel.getLabelId()); @@ -229,11 +237,22 @@ public class CardDataProvider extends AbstractSyncDataProvider<FullCard> { } } - List<JoinCardWithUser> deletedUsers = dataBaseAdapter.getAllDeletedUserJoinsWithRemoteIDs(); - for (JoinCardWithUser deletedUser : deletedUsers) { - Card card = dataBaseAdapter.getCardByRemoteIdDirectly(account.getId(), deletedUser.getCardId()); + + List<JoinCardWithUser> changedUsers; + if (this.stack == null) { + changedUsers = dataBaseAdapter.getAllChangedUserJoinsWithRemoteIDs(); + } else { + changedUsers = dataBaseAdapter.getAllChangedUserJoinsWithRemoteIDsForStack(this.stack.getLocalId()); + } + for (JoinCardWithUser changedUser : changedUsers) { + // not already known to server? + if (changedUser.getCardId() == null) { + //skip for now + continue; + } + Card card = dataBaseAdapter.getCardByRemoteIdDirectly(account.getId(), changedUser.getCardId()); if (this.stack == null) { - stack = dataBaseAdapter.getFullStackByLocalIdDirectly(card.getLocalId()); + stack = dataBaseAdapter.getFullStackByLocalIdDirectly(card.getStackId()); } else { stack = this.stack; } @@ -243,16 +262,16 @@ public class CardDataProvider extends AbstractSyncDataProvider<FullCard> { } else { board = this.board; } - User user = dataBaseAdapter.getUserByLocalIdDirectly(deletedUser.getUserId()); - if (deletedUser.getStatusEnum() == DBStatus.LOCAL_DELETED) { - serverAdapter.unassignUserFromCard(board.getId(), stack.getId(), deletedUser.getCardId(), user.getUid(), new IResponseCallback<Void>(account) { + User user = dataBaseAdapter.getUserByLocalIdDirectly(changedUser.getUserId()); + if (changedUser.getStatusEnum() == DBStatus.LOCAL_DELETED) { + serverAdapter.unassignUserFromCard(board.getId(), stack.getId(), changedUser.getCardId(), user.getUid(), new IResponseCallback<Void>(account) { @Override public void onResponse(Void response) { - dataBaseAdapter.deleteJoinedUserForCardPhysicallyByRemoteIDs(account.getId(), deletedUser.getCardId(), user.getUid()); + dataBaseAdapter.deleteJoinedUserForCardPhysicallyByRemoteIDs(account.getId(), changedUser.getCardId(), user.getUid()); } }); - } else if (deletedUser.getStatusEnum() == DBStatus.LOCAL_EDITED) { - serverAdapter.assignUserToCard(board.getId(), stack.getId(), deletedUser.getCardId(), user.getUid(), new IResponseCallback<Void>(account) { + } else if (changedUser.getStatusEnum() == DBStatus.LOCAL_EDITED) { + serverAdapter.assignUserToCard(board.getId(), stack.getId(), changedUser.getCardId(), user.getUid(), new IResponseCallback<Void>(account) { @Override public void onResponse(Void response) { dataBaseAdapter.setStatusForJoinCardWithUser(card.getLocalId(), user.getLocalId(), DBStatus.UP_TO_DATE.getId()); @@ -261,7 +280,12 @@ public class CardDataProvider extends AbstractSyncDataProvider<FullCard> { } } - List<Attachment> attachments = dataBaseAdapter.getLocallyChangedAttachmentsDirectly(account.getId()); + List<Attachment> attachments; + if (this.stack == null) { + attachments = dataBaseAdapter.getLocallyChangedAttachmentsDirectly(account.getId()); + } else { + attachments = dataBaseAdapter.getLocallyChangedAttachmentsForStackDirectly(this.stack.getLocalId()); + } for (Attachment attachment : attachments) { FullCard card = dataBaseAdapter.getFullCardByLocalIdDirectly(account.getId(), attachment.getCardId()); stack = dataBaseAdapter.getFullStackByLocalIdDirectly(card.getCard().getStackId()); @@ -269,7 +293,12 @@ public class CardDataProvider extends AbstractSyncDataProvider<FullCard> { syncHelper.doUpSyncFor(new AttachmentDataProvider(this, board, stack.getStack(), card, Collections.singletonList(attachment))); } - List<Card> cardsWithChangedComments = dataBaseAdapter.getCardsWithLocallyChangedCommentsDirectly(account.getId()); + List<Card> cardsWithChangedComments; + if (this.stack == null) { + cardsWithChangedComments = dataBaseAdapter.getCardsWithLocallyChangedCommentsDirectly(account.getId()); + } else { + cardsWithChangedComments = dataBaseAdapter.getCardsWithLocallyChangedCommentsForStackDirectly(this.stack.getLocalId()); + } for (Card card : cardsWithChangedComments) { syncHelper.doUpSyncFor(new DeckCommentsDataProvider(this, card)); } @@ -279,7 +308,7 @@ public class CardDataProvider extends AbstractSyncDataProvider<FullCard> { @Override public void handleDeletes(ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, long accountId, List<FullCard> entitiesFromServer) { - List<FullCard> localCards = dataBaseAdapter.getFullCardsForStackDirectly(accountId, stack.getLocalId()); + List<FullCard> localCards = dataBaseAdapter.getFullCardsForStackDirectly(accountId, stack.getLocalId(), null); List<FullCard> delta = findDelta(entitiesFromServer, localCards); for (FullCard cardToDelete : delta) { if (cardToDelete.getId() == null) { diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/DeckCommentsDataProvider.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/DeckCommentsDataProvider.java index 5b9be120f..a26a790a1 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/DeckCommentsDataProvider.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/DeckCommentsDataProvider.java @@ -28,6 +28,9 @@ public class DeckCommentsDataProvider extends AbstractSyncDataProvider<OcsCommen serverAdapter.getCommentsForRemoteCardId(card.getId(), new IResponseCallback<OcsComment>(responder.getAccount()) { @Override public void onResponse(OcsComment response) { + if (response == null) { + response = new OcsComment(); + } List<OcsComment> comments = response.split(); Collections.sort(comments, (o1, o2) -> o1.getSingle().getCreationDateTime().compareTo(o2.getSingle().getCreationDateTime())); verifyCommentListIntegrity(comments); 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..dd40b49fe 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; @@ -12,14 +13,14 @@ import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.DataBaseAdapter public class LabelDataProvider extends AbstractSyncDataProvider<Label> { - private List<Label> labels; - private Board board; + private final List<Label> labels; + private final Board board; public LabelDataProvider(AbstractSyncDataProvider<?> parent, Board board, List<Label> labels) { super(parent); this.board = board; this.labels = labels; - if (this.labels!= null && board != null){ + if (this.labels != null && board != null) { for (Label label : labels) { label.setBoardId(board.getLocalId()); } @@ -44,7 +45,7 @@ public class LabelDataProvider extends AbstractSyncDataProvider<Label> { updateInDB(dataBaseAdapter, accountId, entity, false); return entity.getLocalId(); } else { - return dataBaseAdapter.createLabel(accountId, entity); + return dataBaseAdapter.createLabelDirectly(accountId, entity); } } @@ -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/OcsProjectDataProvider.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/OcsProjectDataProvider.java new file mode 100644 index 000000000..a32fcb89d --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/OcsProjectDataProvider.java @@ -0,0 +1,99 @@ +package it.niedermann.nextcloud.deck.persistence.sync.helpers.providers; + +import java.util.Collections; +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.model.Card; +import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProject; +import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProjectList; +import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProjectResource; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.ServerAdapter; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.DataBaseAdapter; + +public class OcsProjectDataProvider extends AbstractSyncDataProvider<OcsProject> { + private Card card; + public OcsProjectDataProvider(AbstractSyncDataProvider<?> parent, Card card) { + super(parent); + this.card = card; + } + + @Override + public void getAllFromServer(ServerAdapter serverAdapter, long accountId, IResponseCallback<List<OcsProject>> responder, Date lastSync) { + serverAdapter.getProjectsForCard(card.getId(), new IResponseCallback<OcsProjectList>(responder.getAccount()) { + @Override + public void onResponse(OcsProjectList response) { + responder.onResponse(response.getProjects()); + } + + @Override + public void onError(Throwable throwable) { + super.onError(throwable); + // dont break the sync! + DeckLog.logError(throwable); + responder.onResponse(Collections.emptyList()); + } + }); + } + + @Override + public OcsProject getSingleFromDB(DataBaseAdapter dataBaseAdapter, long accountId, OcsProject entity) { + return dataBaseAdapter.getProjectByRemoteIdDirectly(accountId, entity.getId()); + } + + @Override + public long createInDB(DataBaseAdapter dataBaseAdapter, long accountId, OcsProject entity) { + Long newId = dataBaseAdapter.createProjectDirectly(accountId, entity); + entity.setLocalId(newId); + updateResources(dataBaseAdapter, accountId, entity); + return newId; + } + + @Override + public void updateInDB(DataBaseAdapter dataBaseAdapter, long accountId, OcsProject entity, boolean setStatus) { + dataBaseAdapter.updateProjectDirectly(accountId, entity); + dataBaseAdapter.deleteProjectResourcesForProjectIdDirectly(entity.getLocalId()); + updateResources(dataBaseAdapter, accountId, entity); + } + + @Override + public void deleteInDB(DataBaseAdapter dataBaseAdapter, long accountId, OcsProject ocsProject) { + if (ocsProject != null && ocsProject.getLocalId() != null) { + dataBaseAdapter.deleteProjectDirectly(ocsProject); + } + } + + private void updateResources(DataBaseAdapter dataBaseAdapter, Long accountId, OcsProject entity) { + if (entity.getResources() != null) { + for (OcsProjectResource resource : entity.getResources()) { + resource.setProjectId(entity.getLocalId()); + resource.setLocalId(dataBaseAdapter.createProjectResourceDirectly(accountId, resource)); + if ("deck-card".equals(resource.getType())){ + dataBaseAdapter.assignCardToProjectIfMissng(accountId, entity.getLocalId(), resource.getId()); + } + } + } + } + + @Override + public void createOnServer(ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, long accountId, IResponseCallback<OcsProject> responder, OcsProject entity) { + // Do Nothing + } + + @Override + public void updateOnServer(ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, long accountId, IResponseCallback<OcsProject> callback, OcsProject entity) { + // Do Nothing + } + + @Override + public void deleteOnServer(ServerAdapter serverAdapter, long accountId, IResponseCallback<Void> callback, OcsProject entity, DataBaseAdapter dataBaseAdapter) { + // Do Nothing + } + + @Override + public List<OcsProject> getAllChangedFromDB(DataBaseAdapter dataBaseAdapter, long accountId, Date lastSync) { + return Collections.emptyList(); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/StackDataProvider.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/StackDataProvider.java index a3c123afd..ba305dc0a 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/StackDataProvider.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/providers/StackDataProvider.java @@ -2,9 +2,9 @@ package it.niedermann.nextcloud.deck.persistence.sync.helpers.providers; import java.util.Collections; import java.util.Date; -import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.concurrent.ConcurrentSkipListSet; import it.niedermann.nextcloud.deck.api.IResponseCallback; import it.niedermann.nextcloud.deck.model.Board; @@ -19,6 +19,8 @@ import it.niedermann.nextcloud.deck.persistence.sync.helpers.SyncHelper; public class StackDataProvider extends AbstractSyncDataProvider<FullStack> { private FullBoard board; + private Set<Long> syncedStacks = new ConcurrentSkipListSet<>(); + public StackDataProvider(AbstractSyncDataProvider<?> parent, FullBoard board) { super(parent); this.board = board; @@ -103,16 +105,19 @@ public class StackDataProvider extends AbstractSyncDataProvider<FullStack> { @Override public void goDeeperForUpSync(SyncHelper syncHelper, ServerAdapter serverAdapter, DataBaseAdapter dataBaseAdapter, IResponseCallback<Boolean> callback) { List<FullCard> changedCards = dataBaseAdapter.getLocallyChangedCardsDirectly(callback.getAccount().getId()); - Set<Long> syncedStacks = new HashSet<>(); - if (changedCards != null && changedCards.size() > 0){ + if (changedCards != null && !changedCards.isEmpty()){ for (FullCard changedCard : changedCards) { long stackId = changedCard.getCard().getStackId(); - boolean added = syncedStacks.add(stackId); - if (added) { + boolean alreadySynced = syncedStacks.contains(stackId); + if (!alreadySynced) { FullStack stack = dataBaseAdapter.getFullStackByLocalIdDirectly(stackId); - Board board = dataBaseAdapter.getBoardByLocalIdDirectly(stack.getStack().getBoardId()); - changedCard.getCard().setStackId(stack.getId()); - syncHelper.doUpSyncFor(new CardDataProvider(this, board, stack)); + // already synced and known to server? + if (stack.getStack().getId() != null) { + syncedStacks.add(stackId); + Board board = dataBaseAdapter.getBoardByLocalIdDirectly(stack.getStack().getBoardId()); + changedCard.getCard().setStackId(stack.getId()); + syncHelper.doUpSyncFor(new CardDataProvider(this, board, stack)); + } } } } else { 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/persistence/sync/helpers/util/AsyncUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/util/AsyncUtil.java new file mode 100644 index 000000000..faabda163 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/persistence/sync/helpers/util/AsyncUtil.java @@ -0,0 +1,21 @@ +package it.niedermann.nextcloud.deck.persistence.sync.helpers.util; + +import java.util.concurrent.CountDownLatch; + +import it.niedermann.nextcloud.deck.DeckLog; + +public class AsyncUtil { + public interface LatchCallback { + void doWork(CountDownLatch latch); + } + + public static void awaitAsyncWork(int count, LatchCallback worker){ + CountDownLatch countDownLatch = new CountDownLatch(count); + worker.doWork(countDownLatch); + try { + countDownLatch.await(); + } catch (InterruptedException e) { + DeckLog.logError(e); + } + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/ImportAccountActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/ImportAccountActivity.java index 854b99a6a..957743919 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/ImportAccountActivity.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/ImportAccountActivity.java @@ -23,6 +23,7 @@ import com.nextcloud.android.sso.exceptions.AndroidGetAccountsPermissionNotGrant import com.nextcloud.android.sso.exceptions.NextcloudFilesAppNotInstalledException; import com.nextcloud.android.sso.helper.SingleAccountHelper; import com.nextcloud.android.sso.model.SingleSignOnAccount; +import com.nextcloud.android.sso.ui.UiExceptionManager; import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.R; @@ -36,7 +37,6 @@ import it.niedermann.nextcloud.deck.persistence.sync.SyncWorker; import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.WrappedLiveData; import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment; import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler; -import it.niedermann.nextcloud.deck.util.ExceptionUtil; import static com.nextcloud.android.sso.AccountImporter.REQUEST_AUTH_TOKEN_SSO; @@ -79,7 +79,10 @@ public class ImportAccountActivity extends AppCompatActivity { try { AccountImporter.pickNewAccount(this); } catch (NextcloudFilesAppNotInstalledException e) { - ExceptionUtil.handleNextcloudFilesAppNotInstalledException(this, e); + UiExceptionManager.showDialogForException(this, e); + DeckLog.warn("============================================================="); + DeckLog.warn("Nextcloud app is not installed. Cannot choose account"); + DeckLog.logError(e); } catch (AndroidGetAccountsPermissionNotGranted e) { binding.addButton.setEnabled(true); AccountImporter.requestAndroidAccountPermissionsAndPickAccount(this); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/MainActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/MainActivity.java index b5713909b..91c08ffad 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 @@ -53,6 +53,7 @@ import java.util.Objects; import it.niedermann.android.crosstabdnd.CrossTabDragAndDrop; import it.niedermann.android.tablayouthelper.TabLayoutHelper; import it.niedermann.android.tablayouthelper.TabTitleGenerator; +import it.niedermann.nextcloud.deck.DeckApplication; import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.api.IResponseCallback; @@ -65,6 +66,7 @@ import it.niedermann.nextcloud.deck.model.Stack; import it.niedermann.nextcloud.deck.model.full.FullBoard; import it.niedermann.nextcloud.deck.model.full.FullCard; import it.niedermann.nextcloud.deck.model.full.FullStack; +import it.niedermann.nextcloud.deck.model.internal.FilterInformation; import it.niedermann.nextcloud.deck.model.ocs.Capabilities; import it.niedermann.nextcloud.deck.model.ocs.Version; import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; @@ -112,8 +114,8 @@ import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.applyBrandTo import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.clearBrandColors; import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.getSecondaryForegroundColorDependingOnTheme; import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.saveBrandColors; -import static it.niedermann.nextcloud.deck.util.ColorUtil.contrastRatioIsSufficient; -import static it.niedermann.nextcloud.deck.util.ColorUtil.contrastRatioIsSufficientBigAreas; +import static it.niedermann.nextcloud.deck.util.DeckColorUtil.contrastRatioIsSufficient; +import static it.niedermann.nextcloud.deck.util.DeckColorUtil.contrastRatioIsSufficientBigAreas; import static it.niedermann.nextcloud.deck.util.DrawerMenuUtil.MENU_ID_ABOUT; import static it.niedermann.nextcloud.deck.util.DrawerMenuUtil.MENU_ID_ADD_BOARD; import static it.niedermann.nextcloud.deck.util.DrawerMenuUtil.MENU_ID_ARCHIVED_BOARDS; @@ -143,7 +145,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener private Observer<List<Board>> boardsLiveDataObserver; private Menu listMenu; - private LiveData<List<FullStack>> stacksLiveData; + private LiveData<List<Stack>> stacksLiveData; private LiveData<Boolean> hasArchivedBoardsLiveData; private Observer<Boolean> hasArchivedBoardsLiveDataObserver; @@ -273,9 +275,13 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener if (!currentBoardIdWasInList) { setCurrentBoard(boardsList.get(0)); } + + binding.filter.setOnClickListener((v) -> FilterDialogFragment.newInstance().show(getSupportFragmentManager(), EditStackDialogFragment.class.getCanonicalName())); } else { clearBrandColors(this); clearCurrentBoard(); + + binding.filter.setOnClickListener(null); } if (hasArchivedBoardsLiveData != null && hasArchivedBoardsLiveDataObserver != null) { @@ -374,8 +380,6 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener }); filterViewModel.getFilterInformation().observe(this, (info) -> binding.filterIndicator.setVisibility(filterViewModel.getFilterInformation().getValue() == null ? View.GONE : View.VISIBLE)); - - binding.filter.setOnClickListener((v) -> FilterDialogFragment.newInstance().show(getSupportFragmentManager(), EditStackDialogFragment.class.getCanonicalName())); binding.archivedCards.setOnClickListener((v) -> startActivity(ArchivedCardsActvitiy.createIntent(this, mainViewModel.getCurrentAccount(), mainViewModel.getCurrentBoardLocalId(), mainViewModel.currentBoardHasEditPermission()))); @@ -422,7 +426,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener applyBrandToPrimaryTabLayout(mainColor, binding.stackTitles); applyBrandToFAB(mainColor, binding.fab); // TODO We assume, that the background of the spinner is always white - binding.swipeRefreshLayout.setColorSchemeColors(contrastRatioIsSufficient(Color.WHITE, mainColor) ? mainColor : colorAccent); + binding.swipeRefreshLayout.setColorSchemeColors(contrastRatioIsSufficient(Color.WHITE, mainColor) ? mainColor : DeckApplication.isDarkTheme(this) ? Color.DKGRAY : colorAccent); headerBinding.headerView.setBackgroundColor(mainColor); @ColorInt final int headerTextColor = contrastRatioIsSufficientBigAreas(mainColor, Color.WHITE) ? Color.WHITE : Color.BLACK; DrawableCompat.setTint(headerBinding.logo.getDrawable(), headerTextColor); @@ -440,43 +444,28 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener @Override public void onCreateStack(String stackName) { - // TODO this outer call is only necessary to get the highest order. Move logic to SyncManager. - observeOnce(syncManager.getStacksForBoard(mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId()), MainActivity.this, fullStacks -> { - final Stack s = new Stack(stackName, mainViewModel.getCurrentBoardLocalId()); - int heighestOrder = 0; - for (FullStack fullStack : fullStacks) { - int currentStackOrder = fullStack.stack.getOrder(); - if (currentStackOrder >= heighestOrder) { - heighestOrder = currentStackOrder + 1; - } + DeckLog.info("Create Stack in account " + mainViewModel.getCurrentAccount().getName() + " on board " + mainViewModel.getCurrentBoardLocalId()); + WrappedLiveData<FullStack> createLiveData = syncManager.createStack(mainViewModel.getCurrentAccount().getId(), stackName, mainViewModel.getCurrentBoardLocalId()); + observeOnce(createLiveData, this, (fullStack) -> { + if (createLiveData.hasError()) { + final Throwable error = createLiveData.getError(); + assert error != null; + BrandedSnackbar.make(binding.coordinatorLayout, Objects.requireNonNull(error.getLocalizedMessage()), Snackbar.LENGTH_LONG) + .setAction(R.string.simple_more, v -> ExceptionDialogFragment.newInstance(error, mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName())) + .show(); + } else { + binding.viewPager.setCurrentItem(stackAdapter.getItemCount()); } - s.setOrder(heighestOrder); - DeckLog.info("Create Stack in account " + mainViewModel.getCurrentAccount().getName() + " on board " + mainViewModel.getCurrentBoardLocalId()); - WrappedLiveData<FullStack> createLiveData = syncManager.createStack(mainViewModel.getCurrentAccount().getId(), s); - observeOnce(createLiveData, this, (fullStack) -> { - if (createLiveData.hasError()) { - final Throwable error = createLiveData.getError(); - assert error != null; - BrandedSnackbar.make(binding.coordinatorLayout, Objects.requireNonNull(error.getLocalizedMessage()), Snackbar.LENGTH_LONG) - .setAction(R.string.simple_more, v -> ExceptionDialogFragment.newInstance(error, mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName())) - .show(); - } else { - binding.viewPager.setCurrentItem(stackAdapter.getItemCount()); - } - }); }); } @Override public void onUpdateStack(long localStackId, String stackName) { - observeOnce(syncManager.getStack(mainViewModel.getCurrentAccount().getId(), localStackId), MainActivity.this, fullStack -> { - fullStack.getStack().setTitle(stackName); - final WrappedLiveData<FullStack> archiveLiveData = syncManager.updateStack(fullStack); - observeOnce(archiveLiveData, this, (v) -> { - if (archiveLiveData.hasError()) { - ExceptionDialogFragment.newInstance(archiveLiveData.getError(), mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); - } - }); + final WrappedLiveData<FullStack> liveData = syncManager.updateStackTitle(localStackId, stackName); + observeOnce(liveData, this, (v) -> { + if (liveData.hasError()) { + ExceptionDialogFragment.newInstance(liveData.getError(), mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + } }); } @@ -508,7 +497,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) { @@ -548,7 +542,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener if (stacksLiveData != null) { stacksLiveData.removeObservers(this); } - saveBrandColors(this, Color.parseColor('#' + board.getColor())); + saveBrandColors(this, board.getColor()); mainViewModel.setCurrentBoard(board); filterViewModel.clearFilterInformation(); @@ -570,11 +564,11 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener binding.swipeRefreshLayout.setVisibility(View.VISIBLE); stacksLiveData = syncManager.getStacksForBoard(mainViewModel.getCurrentAccount().getId(), board.getLocalId()); - stacksLiveData.observe(this, (List<FullStack> fullStacks) -> { - if (fullStacks == null) { + stacksLiveData.observe(this, (List<Stack> stacks) -> { + if (stacks == null) { throw new IllegalStateException("Given List<FullStack> must not be null"); } - currentBoardStacksCount = fullStacks.size(); + currentBoardStacksCount = stacks.size(); if (currentBoardStacksCount == 0) { binding.emptyContentViewStacks.setVisibility(View.VISIBLE); @@ -586,19 +580,19 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener listMenu.findItem(R.id.archive_cards).setVisible(currentBoardHasStacks); int stackPositionInAdapter = 0; - stackAdapter.setStacks(fullStacks); + stackAdapter.setStacks(stacks); long currentStackId = readCurrentStackId(this, mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId()); for (int i = 0; i < currentBoardStacksCount; i++) { - if (fullStacks.get(i).getLocalId() == currentStackId || currentStackId == NO_STACK_ID) { + if (stacks.get(i).getLocalId() == currentStackId || currentStackId == NO_STACK_ID) { stackPositionInAdapter = i; break; } } final int stackPositionInAdapterClone = stackPositionInAdapter; final TabTitleGenerator tabTitleGenerator = position -> { - if (fullStacks.size() > position) { - return fullStacks.get(position).getStack().getTitle(); + if (stacks.size() > position) { + return stacks.get(position).getTitle(); } else { DeckLog.logError(new IllegalStateException("Could not generate tab title for position " + position + " because list size is only " + currentBoardStacksCount)); return "ERROR"; @@ -673,16 +667,19 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.archive_cards: { - final FullStack fullStack = stackAdapter.getItem(binding.viewPager.getCurrentItem()); - final long stackLocalId = fullStack.getLocalId(); + final Stack stack = stackAdapter.getItem(binding.viewPager.getCurrentItem()); + final long stackLocalId = stack.getLocalId(); observeOnce(syncManager.countCardsInStack(mainViewModel.getCurrentAccount().getId(), stackLocalId), MainActivity.this, (numberOfCards) -> { new BrandedAlertDialogBuilder(this) .setTitle(R.string.archive_cards) - .setMessage(getString(R.string.do_you_want_to_archive_all_cards_of_the_list, fullStack.getStack().getTitle())) + .setMessage(getString(FilterInformation.hasActiveFilter(filterViewModel.getFilterInformation().getValue()) + ? R.string.do_you_want_to_archive_all_cards_of_the_filtered_list + : R.string.do_you_want_to_archive_all_cards_of_the_list, stack.getTitle())) .setPositiveButton(R.string.simple_archive, (dialog, whichButton) -> { - final WrappedLiveData<Void> archiveStackLiveData = syncManager.archiveCardsInStack(mainViewModel.getCurrentAccount().getId(), stackLocalId); + final FilterInformation filterInformation = filterViewModel.getFilterInformation().getValue(); + final WrappedLiveData<Void> archiveStackLiveData = syncManager.archiveCardsInStack(mainViewModel.getCurrentAccount().getId(), stackLocalId, filterInformation == null ? new FilterInformation() : filterInformation); observeOnce(archiveStackLiveData, this, (result) -> { - if (archiveStackLiveData.hasError()) { + if (archiveStackLiveData.hasError() && !SyncManager.ignoreExceptionOnVoidError(archiveStackLiveData.getError())) { ExceptionDialogFragment.newInstance(archiveStackLiveData.getError(), mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); } }); @@ -926,7 +923,7 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener long stackId = stackAdapter.getItem(binding.viewPager.getCurrentItem()).getLocalId(); final WrappedLiveData<Void> deleteStackLiveData = syncManager.deleteStack(mainViewModel.getCurrentAccount().getId(), stackId, mainViewModel.getCurrentBoardLocalId()); observeOnce(deleteStackLiveData, this, (v) -> { - if (deleteStackLiveData.hasError()) { + if (deleteStackLiveData.hasError() && !SyncManager.ignoreExceptionOnVoidError(deleteStackLiveData.getError())) { ExceptionDialogFragment.newInstance(deleteStackLiveData.getError(), mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); } }); @@ -946,7 +943,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() && !SyncManager.ignoreExceptionOnVoidError(deleteLiveData.getError())) { + ExceptionDialogFragment.newInstance(deleteLiveData.getError(), mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + } + }); + binding.drawerLayout.closeDrawer(GravityCompat.START); } @@ -966,6 +970,39 @@ public class MainActivity extends BrandedActivity implements DeleteStackListener @Override public void onArchive(@NonNull Board board) { - syncManager.archiveBoard(board); + final WrappedLiveData<FullBoard> liveData = syncManager.archiveBoard(board); + observeOnce(liveData, this, (fullBoard) -> { + if (liveData.hasError()) { + ExceptionDialogFragment.newInstance(liveData.getError(), mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + } + }); + } + + @Override + public void onClone(Board board) { + final String[] animals = {getString(R.string.clone_cards)}; + final boolean[] checkedItems = {false}; + new BrandedAlertDialogBuilder(this) + .setTitle(R.string.clone_board) + .setMultiChoiceItems(animals, checkedItems, (dialog, which, isChecked) -> checkedItems[0] = isChecked) + .setPositiveButton(R.string.simple_clone, (dialog, which) -> { + binding.drawerLayout.closeDrawer(GravityCompat.START); + final Snackbar snackbar = BrandedSnackbar.make(binding.coordinatorLayout, getString(R.string.cloning_board, board.getTitle()), Snackbar.LENGTH_INDEFINITE); + snackbar.show(); + final WrappedLiveData<FullBoard> liveData = syncManager.cloneBoard(board.getAccountId(), board.getLocalId(), board.getAccountId(), board.getColor(), checkedItems[0]); + observeOnce(liveData, this, (fullBoard -> { + snackbar.dismiss(); + if (liveData.hasError()) { + ExceptionDialogFragment.newInstance(liveData.getError(), mainViewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + } else { + setCurrentBoard(fullBoard.getBoard()); + BrandedSnackbar.make(binding.coordinatorLayout, getString(R.string.successfully_cloned_board, fullBoard.getBoard().getTitle()), Snackbar.LENGTH_LONG) + .setAction(R.string.edit, v -> EditBoardDialogFragment.newInstance(fullBoard.getLocalId()).show(getSupportFragmentManager(), EditBoardDialogFragment.class.getSimpleName())) + .show(); + } + })); + }) + .setNeutralButton(android.R.string.cancel, null) + .show(); } }
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/MainViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/MainViewModel.java index e5bd4f482..b73b86b33 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/MainViewModel.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/MainViewModel.java @@ -15,6 +15,7 @@ import it.niedermann.nextcloud.deck.model.Board; public class MainViewModel extends AndroidViewModel { private MutableLiveData<Account> currentAccount = new MutableLiveData<>(); + @Nullable private Board currentBoard; private boolean currentAccountHasArchivedBoards = false; @@ -37,16 +38,21 @@ public class MainViewModel extends AndroidViewModel { this.currentAccountIsSupportedVersion = currentAccount.getServerDeckVersionAsObject().isSupported(getApplication().getApplicationContext()); } - public void setCurrentBoard(Board currentBoard) { + public void setCurrentBoard(@NonNull Board currentBoard) { this.currentBoard = currentBoard; } public Long getCurrentBoardLocalId() { + if(currentBoard == null) { + throw new IllegalStateException("getCurrentBoardLocalId() called before setCurrentBoard()"); + } return this.currentBoard.getLocalId(); } - @Nullable public Long getCurrentBoardRemoteId() { + if(currentBoard == null) { + throw new IllegalStateException("getCurrentBoardRemoteId() called before setCurrentBoard()"); + } return this.currentBoard.getId(); } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/PickStackActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/PickStackActivity.java new file mode 100644 index 000000000..e0a9b6a06 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/PickStackActivity.java @@ -0,0 +1,119 @@ +package it.niedermann.nextcloud.deck.ui; + +import android.content.Intent; +import android.content.res.ColorStateList; +import android.graphics.Color; +import android.os.Bundle; + +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.content.ContextCompat; +import androidx.core.graphics.drawable.DrawableCompat; + +import java.util.List; + +import it.niedermann.android.util.ColorUtil; +import it.niedermann.nextcloud.deck.DeckLog; +import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.databinding.ActivityPickStackBinding; +import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.model.Board; +import it.niedermann.nextcloud.deck.model.Stack; +import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; +import it.niedermann.nextcloud.deck.ui.branding.Branded; +import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler; +import it.niedermann.nextcloud.deck.ui.pickstack.PickStackFragment; +import it.niedermann.nextcloud.deck.ui.pickstack.PickStackListener; + +import static androidx.lifecycle.Transformations.switchMap; +import static it.niedermann.nextcloud.deck.DeckApplication.isDarkTheme; +import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.getSecondaryForegroundColorDependingOnTheme; +import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.isBrandingEnabled; +import static it.niedermann.nextcloud.deck.util.DeckColorUtil.contrastRatioIsSufficientBigAreas; + +public abstract class PickStackActivity extends AppCompatActivity implements Branded, PickStackListener { + + protected ActivityPickStackBinding binding; + + protected SyncManager syncManager; + + private boolean brandingEnabled; + + private Account selectedAccount; + private Board selectedBoard; + private Stack selectedStack; + + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Thread.setDefaultUncaughtExceptionHandler(new ExceptionHandler(this)); + + brandingEnabled = isBrandingEnabled(this); + + binding = ActivityPickStackBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + setSupportActionBar(binding.toolbar); + + syncManager = new SyncManager(this); + + switchMap(syncManager.hasAccounts(), hasAccounts -> { + if (hasAccounts) { + return syncManager.readAccounts(); + } else { + startActivityForResult(new Intent(this, ImportAccountActivity.class), ImportAccountActivity.REQUEST_CODE_IMPORT_ACCOUNT); + return null; + } + }).observe(this, (List<Account> accounts) -> { + if (accounts == null || accounts.size() == 0) { + throw new IllegalStateException("hasAccounts() returns true, but readAccounts() returns null or has no entry"); + } + getSupportFragmentManager() + .beginTransaction() + .add(R.id.fragment_container, PickStackFragment.newInstance(showBoardsWithoutEditPermission())) + .commit(); + }); + binding.cancel.setOnClickListener((v) -> finish()); + binding.submit.setOnClickListener((v) -> onSubmit(selectedAccount, selectedBoard.getLocalId(), selectedStack.getLocalId())); + } + + @Override + public void onStackPicked(@NonNull Account account, @Nullable Board board, @Nullable Stack stack) { + this.selectedAccount = account; + this.selectedBoard = board; + this.selectedStack = stack; + if (board == null) { + binding.submit.setEnabled(false); + } else { + applyBrand(board.getColor()); + if (stack == null) { + binding.submit.setEnabled(false); + } else { + binding.submit.setEnabled(true); + } + } + } + + @Override + public void applyBrand(int mainColor) { + try { + if (brandingEnabled) { + @ColorInt final int finalMainColor = contrastRatioIsSufficientBigAreas(mainColor, ContextCompat.getColor(this, R.color.primary)) + ? mainColor + : isDarkTheme(this) ? Color.WHITE : Color.BLACK; + DrawableCompat.setTintList(binding.submit.getBackground(), ColorStateList.valueOf(finalMainColor)); + binding.submit.setTextColor(ColorUtil.INSTANCE.getForegroundColorForBackgroundColor(finalMainColor)); + binding.cancel.setTextColor(getSecondaryForegroundColorDependingOnTheme(this, mainColor)); + } + } catch (Throwable t) { + DeckLog.logError(t); + } + } + + abstract protected void onSubmit(Account account, long boardId, long stackId); + + abstract protected boolean showBoardsWithoutEditPermission(); +}
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/PushNotificationActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/PushNotificationActivity.java index b0b0d68ae..a02f5aad3 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/PushNotificationActivity.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/PushNotificationActivity.java @@ -5,21 +5,24 @@ import android.net.Uri; import android.text.TextUtils; import android.view.View; +import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.UiThread; import androidx.appcompat.app.AppCompatActivity; import com.nextcloud.android.sso.helper.SingleAccountHelper; +import it.niedermann.android.util.ColorUtil; import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.api.IResponseCallback; import it.niedermann.nextcloud.deck.databinding.ActivityPushNotificationBinding; import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; import it.niedermann.nextcloud.deck.ui.card.EditActivity; import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler; +import it.niedermann.nextcloud.deck.util.ProjectUtil; -import static android.graphics.Color.parseColor; import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce; public class PushNotificationActivity extends AppCompatActivity { @@ -56,6 +59,7 @@ public class PushNotificationActivity extends AppCompatActivity { } final String link = getIntent().getStringExtra(KEY_LINK); + long[] ids = ProjectUtil.extractBoardIdAndCardIdFromUrl(link); binding.cancel.setOnClickListener((v) -> finish()); @@ -64,54 +68,98 @@ public class PushNotificationActivity extends AppCompatActivity { final String accountString = getIntent().getStringExtra(KEY_ACCOUNT); DeckLog.verbose("cardRemoteIdString = " + cardRemoteIdString); - if (cardRemoteIdString != null) { - try { - final int cardRemoteId = Integer.parseInt(cardRemoteIdString); - observeOnce(accountReadingSyncManager.readAccount(accountString), this, (account -> { - if (account != null) { - SingleAccountHelper.setCurrentAccount(this, account.getName()); - final SyncManager syncManager = new SyncManager(this); - DeckLog.verbose("account: " + account); - observeOnce(syncManager.getLocalBoardIdByCardRemoteIdAndAccount(cardRemoteId, account), PushNotificationActivity.this, (boardLocalId -> { - DeckLog.verbose("BoardLocalId " + boardLocalId); - if (boardLocalId != null) { - observeOnce(syncManager.synchronizeCardByRemoteId(cardRemoteId, account), PushNotificationActivity.this, (fullCard -> { - DeckLog.verbose("FullCard: " + fullCard); - if (fullCard != null) { - runOnUiThread(() -> { - binding.submit.setOnClickListener((v) -> launchEditActivity(account, boardLocalId, fullCard.getLocalId())); - binding.submit.setText(R.string.simple_open); - applyBrandToSubmitButton(account); - binding.submit.setEnabled(true); - binding.progress.setVisibility(View.INVISIBLE); - }); - } else { - DeckLog.warn("Something went wrong while synchronizing the card " + cardRemoteId + " (cardRemoteId). Given fullCard is null."); - applyBrandToSubmitButton(account); - fallbackToBrowser(link); - } - })); - } else { - DeckLog.warn("Given localBoardId for cardRemoteId " + cardRemoteId + " is null."); - applyBrandToSubmitButton(account); - fallbackToBrowser(link); - } - })); - } else { - DeckLog.warn("Given account for " + accountString + " is null."); - fallbackToBrowser(link); - } - })); - } catch (NumberFormatException e) { - DeckLog.logError(e); + if (ids.length == 2) { + if (cardRemoteIdString != null) { + try { + final int cardRemoteId = Integer.parseInt(cardRemoteIdString); + observeOnce(accountReadingSyncManager.readAccount(accountString), this, (account -> { + if (account != null) { + SingleAccountHelper.setCurrentAccount(this, account.getName()); + final SyncManager syncManager = new SyncManager(this); + DeckLog.verbose("account: " + account); + observeOnce(syncManager.getBoardByRemoteId(account.getId(), ids[0]), PushNotificationActivity.this, (board -> { + DeckLog.verbose("BoardLocalId " + board); + if (board != null) { + observeOnce(syncManager.getCardByRemoteID(account.getId(), cardRemoteId), PushNotificationActivity.this, (card -> { + DeckLog.verbose("Card: " + card); + if (card != null) { + syncManager.synchronizeCard(new IResponseCallback<Boolean>(account) { + @Override + public void onResponse(Boolean response) { + openCardOnSubmit(account, board.getLocalId(), card.getLocalId()); + } + + @Override + public void onError(Throwable throwable) { + super.onError(throwable); + openCardOnSubmit(account, board.getLocalId(), card.getLocalId()); + } + }, card); + } else { + DeckLog.info("Card is not yet available locally. Synchronize board with localId " + board); + + syncManager.synchronizeBoard(new IResponseCallback<Boolean>(account) { + @Override + public void onResponse(Boolean response) { + runOnUiThread(() -> { + observeOnce(syncManager.getCardByRemoteID(account.getId(), cardRemoteId), PushNotificationActivity.this, (card -> { + DeckLog.verbose("Card: " + card); + if (card != null) { + openCardOnSubmit(account, board.getLocalId(), card.getLocalId()); + } else { + DeckLog.warn("Something went wrong while synchronizing the card " + cardRemoteId + " (cardRemoteId). Given fullCard is null."); + applyBrandToSubmitButton(account); + fallbackToBrowser(link); + } + })); + }); + } + + @Override + public void onError(Throwable throwable) { + super.onError(throwable); + DeckLog.warn("Something went wrong while synchronizing the board with localId " + board + "."); + applyBrandToSubmitButton(account); + fallbackToBrowser(link); + } + }, board.getLocalId()); + } + })); + } else { + DeckLog.warn("Given localBoardId for cardRemoteId " + cardRemoteId + " is null."); + applyBrandToSubmitButton(account); + fallbackToBrowser(link); + } + })); + } else { + DeckLog.warn("Given account for " + accountString + " is null."); + fallbackToBrowser(link); + } + })); + } catch (NumberFormatException e) { + DeckLog.logError(e); + fallbackToBrowser(link); + } + } else { + DeckLog.warn(KEY_CARD_REMOTE_ID + " is null."); fallbackToBrowser(link); } } else { - DeckLog.warn(KEY_CARD_REMOTE_ID + " is null."); + DeckLog.warn("Link does not contain two IDs (expected one board id and one card id): " + link); fallbackToBrowser(link); } } + private void openCardOnSubmit(@NonNull Account account, long boardLocalId, long cardlocalId) { + runOnUiThread(() -> { + binding.submit.setOnClickListener((v) -> launchEditActivity(account, boardLocalId, cardlocalId)); + binding.submit.setText(R.string.simple_open); + applyBrandToSubmitButton(account); + binding.submit.setEnabled(true); + binding.progress.setVisibility(View.INVISIBLE); + }); + } + /** * If anything goes wrong and we cannot open the card directly, we fall back to open the given link in the webbrowser */ @@ -146,10 +194,13 @@ public class PushNotificationActivity extends AppCompatActivity { return true; } + // TODO implement Branded interface + // TODO apply branding based on board color public void applyBrandToSubmitButton(@NonNull Account account) { + @ColorInt final int mainColor = account.getColor(); try { - binding.submit.setBackgroundColor(parseColor(account.getColor())); - binding.submit.setTextColor(parseColor(account.getTextColor())); + binding.submit.setBackgroundColor(mainColor); + binding.submit.setTextColor(ColorUtil.INSTANCE.getForegroundColorForBackgroundColor(mainColor)); } catch (Throwable t) { DeckLog.logError(t); } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/about/AboutFragmentLicenseTab.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/about/AboutFragmentLicenseTab.java index c00ff212a..0bd92bc78 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/about/AboutFragmentLicenseTab.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/about/AboutFragmentLicenseTab.java @@ -14,13 +14,13 @@ import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; import androidx.core.graphics.drawable.DrawableCompat; +import it.niedermann.android.util.ColorUtil; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.FragmentAboutLicenseTabBinding; import it.niedermann.nextcloud.deck.ui.branding.BrandedFragment; -import it.niedermann.nextcloud.deck.util.ColorUtil; import static it.niedermann.nextcloud.deck.DeckApplication.isDarkTheme; -import static it.niedermann.nextcloud.deck.util.ColorUtil.contrastRatioIsSufficientBigAreas; +import static it.niedermann.nextcloud.deck.util.DeckColorUtil.contrastRatioIsSufficientBigAreas; import static it.niedermann.nextcloud.deck.util.SpannableUtil.setTextWithURL; public class AboutFragmentLicenseTab extends BrandedFragment { @@ -42,6 +42,6 @@ public class AboutFragmentLicenseTab extends BrandedFragment { ? mainColor : isDarkTheme(requireContext()) ? Color.WHITE : Color.BLACK; DrawableCompat.setTintList(binding.aboutAppLicenseButton.getBackground(), ColorStateList.valueOf(finalMainColor)); - binding.aboutAppLicenseButton.setTextColor(ColorUtil.getForegroundColorForBackgroundColor(finalMainColor)); + binding.aboutAppLicenseButton.setTextColor(ColorUtil.INSTANCE.getForegroundColorForBackgroundColor(finalMainColor)); } }
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherDialog.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherDialog.java index 592f2e8cc..e54923d1e 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherDialog.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherDialog.java @@ -16,18 +16,19 @@ import com.bumptech.glide.request.RequestOptions; import com.nextcloud.android.sso.AccountImporter; import com.nextcloud.android.sso.exceptions.AndroidGetAccountsPermissionNotGranted; import com.nextcloud.android.sso.exceptions.NextcloudFilesAppNotInstalledException; +import com.nextcloud.android.sso.ui.UiExceptionManager; +import it.niedermann.android.util.DimensionUtil; +import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.DialogAccountSwitcherBinding; import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; import it.niedermann.nextcloud.deck.ui.MainViewModel; import it.niedermann.nextcloud.deck.ui.branding.BrandedDialogFragment; import it.niedermann.nextcloud.deck.ui.manageaccounts.ManageAccountsActivity; -import it.niedermann.nextcloud.deck.util.ExceptionUtil; import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce; import static it.niedermann.nextcloud.deck.ui.MainActivity.ACTIVITY_MANAGE_ACCOUNTS; -import static it.niedermann.nextcloud.deck.util.DimensionUtil.dpToPx; public class AccountSwitcherDialog extends BrandedDialogFragment { @@ -52,7 +53,7 @@ public class AccountSwitcherDialog extends BrandedDialogFragment { binding.check.setSelected(true); Glide.with(requireContext()) - .load(viewModel.getCurrentAccount().getAvatarUrl(dpToPx(binding.currentAccountItemAvatar.getContext(), R.dimen.avatar_size))) + .load(viewModel.getCurrentAccount().getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(binding.currentAccountItemAvatar.getContext(), R.dimen.avatar_size))) .placeholder(R.drawable.ic_baseline_account_circle_24) .error(R.drawable.ic_baseline_account_circle_24) .apply(RequestOptions.circleCropTransform()) @@ -76,7 +77,10 @@ public class AccountSwitcherDialog extends BrandedDialogFragment { try { AccountImporter.pickNewAccount(requireActivity()); } catch (NextcloudFilesAppNotInstalledException e) { - ExceptionUtil.handleNextcloudFilesAppNotInstalledException(requireContext(), e); + UiExceptionManager.showDialogForException(requireContext(), e); + DeckLog.warn("============================================================="); + DeckLog.warn("Nextcloud app is not installed. Cannot choose account"); + DeckLog.logError(e); } catch (AndroidGetAccountsPermissionNotGranted e) { AccountImporter.requestAndroidAccountPermissionsAndPickAccount(requireActivity()); } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherViewHolder.java index 9c93c422e..a60b6a0ea 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherViewHolder.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherViewHolder.java @@ -10,12 +10,11 @@ import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; import com.bumptech.glide.request.RequestOptions; -import it.niedermann.android.glidesso.SingleSignOnUrl; +import it.niedermann.android.util.DimensionUtil; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.ItemAccountChooseBinding; import it.niedermann.nextcloud.deck.model.Account; - -import static it.niedermann.nextcloud.deck.util.DimensionUtil.dpToPx; +import it.niedermann.nextcloud.sso.glide.SingleSignOnUrl; public class AccountSwitcherViewHolder extends RecyclerView.ViewHolder { @@ -30,7 +29,7 @@ public class AccountSwitcherViewHolder extends RecyclerView.ViewHolder { binding.accountName.setText(account.getUserName()); binding.accountHost.setText(Uri.parse(account.getUrl()).getHost()); Glide.with(itemView.getContext()) - .load(new SingleSignOnUrl(account.getName(), account.getAvatarUrl(dpToPx(binding.accountItemAvatar.getContext(), R.dimen.avatar_size)))) + .load(new SingleSignOnUrl(account.getName(), account.getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(binding.accountItemAvatar.getContext(), R.dimen.avatar_size)))) .placeholder(R.drawable.ic_baseline_account_circle_24) .error(R.drawable.ic_baseline_account_circle_24) .apply(RequestOptions.circleCropTransform()) diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardViewHolder.java index 5ab94b4f6..67b2c00be 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardViewHolder.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedboards/ArchivedBoardViewHolder.java @@ -30,7 +30,7 @@ public class ArchivedBoardViewHolder extends RecyclerView.ViewHolder { void bind(boolean isSupportedVersion, Board board, FragmentManager fragmentManager, Consumer<Board> dearchiveBoardListener) { final Context context = itemView.getContext(); - binding.boardIcon.setImageDrawable(ViewUtil.getTintedImageView(binding.boardIcon.getContext(), R.drawable.circle_grey600_36dp, "#" + board.getColor())); + binding.boardIcon.setImageDrawable(ViewUtil.getTintedImageView(binding.boardIcon.getContext(), R.drawable.circle_grey600_36dp, String.format("#%06X", (0xFFFFFF & board.getColor())))); binding.boardMenu.setVisibility(View.GONE); binding.boardTitle.setText(board.getTitle()); if (isSupportedVersion) { 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..ed55c2411 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"; @@ -56,7 +60,14 @@ public class ArchivedBoardsActvitiy extends BrandedActivity implements DeleteBoa viewModel.setCurrentAccount(account); syncManager = new SyncManager(this); - adapter = new ArchivedBoardsAdapter(viewModel.isCurrentAccountIsSupportedVersion(), getSupportFragmentManager(), (board) -> syncManager.dearchiveBoard(board)); + adapter = new ArchivedBoardsAdapter(viewModel.isCurrentAccountIsSupportedVersion(), getSupportFragmentManager(), (board) -> { + final WrappedLiveData<FullBoard> liveData = syncManager.dearchiveBoard(board); + observeOnce(liveData, this, (fullBoard) -> { + if (liveData.hasError()) { + ExceptionDialogFragment.newInstance(liveData.getError(), viewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + } + }); + }); binding.recyclerView.setAdapter(adapter); syncManager.getBoards(account.getId(), true).observe(this, (boards) -> { @@ -80,16 +91,36 @@ 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() && !SyncManager.ignoreExceptionOnVoidError(deleteLiveData.getError())) { + ExceptionDialogFragment.newInstance(deleteLiveData.getError(), viewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + } + }); } @Override public void onUpdateBoard(FullBoard fullBoard) { - syncManager.updateBoard(fullBoard); + final WrappedLiveData<FullBoard> updateLiveData = 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); + final WrappedLiveData<FullBoard> liveData = syncManager.dearchiveBoard(board); + observeOnce(liveData, this, (fullBoard) -> { + if (liveData.hasError()) { + ExceptionDialogFragment.newInstance(liveData.getError(), viewModel.getCurrentAccount()).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + } + }); + } + + @Override + public void onClone(Board board) { + } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedcards/ArchivedCardsAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/archivedcards/ArchivedCardsAdapter.java index bc2891360..324d8362c 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); } @@ -45,7 +45,7 @@ public class ArchivedCardsAdapter extends CardAdapter { case R.id.action_card_delete: { final WrappedLiveData<Void> liveData = syncManager.deleteCard(fullCard.getCard()); observeOnce(liveData, lifecycleOwner, (next) -> { - if (liveData.hasError()) { + if (liveData.hasError() && !SyncManager.ignoreExceptionOnVoidError(liveData.getError())) { ExceptionDialogFragment.newInstance(liveData.getError(), account).show(fragmentManager, ExceptionDialogFragment.class.getSimpleName()); } }); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentAdapter.java index 0794323ec..b17b34137 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentAdapter.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentAdapter.java @@ -52,7 +52,7 @@ public class AttachmentAdapter extends RecyclerView.Adapter<AttachmentViewHolder @Override public void onBindViewHolder(@NonNull AttachmentViewHolder holder, int position) { final Attachment attachment = attachments.get(position); - final String uri = AttachmentUtil.getRemoteUrl(account.getUrl(), cardRemoteId, attachment.getId()); + final String uri = AttachmentUtil.getRemoteOrLocalUrl(account.getUrl(), cardRemoteId, attachment); if (MimeTypeUtil.isImage(attachment.getMimetype())) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { holder.binding.preview.setTransitionName(context.getString(R.string.transition_attachment_preview, String.valueOf(attachment.getLocalId()))); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentsActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentsActivity.java index 98cfd4440..2eefd0c83 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentsActivity.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/attachments/AttachmentsActivity.java @@ -2,6 +2,7 @@ package it.niedermann.nextcloud.deck.ui.attachments; import android.content.Context; import android.content.Intent; +import android.graphics.drawable.Drawable; import android.os.Bundle; import android.view.MotionEvent; import android.view.View; @@ -9,6 +10,8 @@ import android.view.View; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.SharedElementCallback; +import androidx.core.content.ContextCompat; +import androidx.core.graphics.drawable.DrawableCompat; import androidx.recyclerview.widget.RecyclerView; import androidx.viewpager2.widget.ViewPager2; @@ -44,6 +47,9 @@ public class AttachmentsActivity extends AppCompatActivity { supportPostponeEnterTransition(); setSupportActionBar(binding.toolbar); + final Drawable navigationIcon = getResources().getDrawable(R.drawable.ic_arrow_back_white_24dp); + DrawableCompat.setTint(navigationIcon, ContextCompat.getColor(this, android.R.color.white)); + binding.toolbar.setNavigationIcon(navigationIcon); final Bundle args = getIntent().getExtras(); if (args == null || !args.containsKey(BUNDLE_KEY_ACCOUNT) || !args.containsKey(BUNDLE_KEY_CARD_ID)) { @@ -59,7 +65,7 @@ public class AttachmentsActivity extends AppCompatActivity { long cardId = args.getLong(BUNDLE_KEY_CARD_ID); final SyncManager syncManager = new SyncManager(this); - syncManager.getCardByLocalId(account.getId(), cardId).observe(this, fullCard -> { + syncManager.getFullCardWithProjectsByLocalId(account.getId(), cardId).observe(this, fullCard -> { final List<Attachment> attachments = new ArrayList<>(); for (Attachment a : fullCard.getAttachments()) { if (MimeTypeUtil.isImage(a.getMimetype())) { @@ -67,7 +73,7 @@ public class AttachmentsActivity extends AppCompatActivity { } } if (fullCard.getAttachments().size() == 0) { - DeckLog.logError(new IllegalStateException(AttachmentsActivity.class.getSimpleName() + " called, but card " + fullCard.getLocalId() + "has no attachments")); + DeckLog.logError(new IllegalStateException(AttachmentsActivity.class.getSimpleName() + " called, but card " + fullCard.getCard().getTitle() + " has no attachments")); supportFinishAfterTransition(); return; } @@ -79,7 +85,7 @@ public class AttachmentsActivity extends AppCompatActivity { binding.toolbar.setTitle(attachments.get(position).getBasename()); } }; - RecyclerView.Adapter adapter = new AttachmentAdapter(account, fullCard.getId(), attachments); + RecyclerView.Adapter<AttachmentViewHolder> adapter = new AttachmentAdapter(account, fullCard.getId(), attachments); binding.viewPager.setAdapter(adapter); binding.viewPager.registerOnPageChangeCallback(onPageChangeCallback); @@ -104,7 +110,7 @@ public class AttachmentsActivity extends AppCompatActivity { long currentAttachmentLocalId = attachments.get(binding.viewPager.getCurrentItem()).getLocalId(); String transitionKey = getString(R.string.transition_attachment_preview, String.valueOf(currentAttachmentLocalId)); if (transitionKey.equals(names.get(0))) { - sharedElements.put(transitionKey, binding.viewPager.getRootView().findViewById(R.id.preview) + sharedElements.put(transitionKey, binding.viewPager.getRootView().findViewById(R.id.avatar) ); } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/ArchiveBoardListener.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/ArchiveBoardListener.java index b7e27aa97..ff0d3e941 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/ArchiveBoardListener.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/ArchiveBoardListener.java @@ -4,4 +4,5 @@ import it.niedermann.nextcloud.deck.model.Board; public interface ArchiveBoardListener { void onArchive(Board board); + void onClone(Board board); }
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/BoardAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/BoardAdapter.java index 7049cc16c..5975e0306 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/BoardAdapter.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/BoardAdapter.java @@ -37,7 +37,7 @@ public class BoardAdapter extends ArrayAdapter<Board> { TextView boardName = convertView.findViewById(R.id.boardName); if (board != null) { boardName.setText(board.getTitle()); - boardName.setCompoundDrawables(ViewUtil.getTintedImageView(context, R.drawable.circle_grey600_36dp, "#" + board.getColor()), null, null, null); + boardName.setCompoundDrawables(ViewUtil.getTintedImageView(context, R.drawable.circle_grey600_36dp, String.format("#%06X", (0xFFFFFF & board.getColor()))), null, null, null); } else { DeckLog.logError(new IllegalArgumentException("board at position " + position + "is null")); } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/EditBoardDialogFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/EditBoardDialogFragment.java index e5d0a482b..e197727d5 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/EditBoardDialogFragment.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/EditBoardDialogFragment.java @@ -7,6 +7,7 @@ import android.os.Bundle; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; +import androidx.core.content.ContextCompat; import androidx.fragment.app.DialogFragment; import androidx.lifecycle.ViewModelProvider; @@ -53,7 +54,7 @@ public class EditBoardDialogFragment extends BrandedDialogFragment { if (args != null && args.containsKey(KEY_BOARD_ID)) { dialogBuilder.setTitle(R.string.edit_board); dialogBuilder.setPositiveButton(R.string.simple_save, (dialog, which) -> { - this.fullBoard.board.setColor(binding.colorChooser.getSelectedColor().substring(1)); + this.fullBoard.board.setColor(binding.colorChooser.getSelectedColor()); this.fullBoard.board.setTitle(binding.input.getText().toString()); editBoardListener.onUpdateBoard(fullBoard); }); @@ -64,13 +65,13 @@ public class EditBoardDialogFragment extends BrandedDialogFragment { String title = this.fullBoard.getBoard().getTitle(); binding.input.setText(title); binding.input.setSelection(title.length()); - binding.colorChooser.selectColor("#" + fullBoard.getBoard().getColor()); + binding.colorChooser.selectColor(String.format("#%06X", (0xFFFFFF & fullBoard.getBoard().getColor()))); } }); } else { dialogBuilder.setTitle(R.string.add_board); dialogBuilder.setPositiveButton(R.string.simple_add, (dialog, which) -> editBoardListener.onCreateBoard(binding.input.getText().toString(), binding.colorChooser.getSelectedColor())); - binding.colorChooser.selectColor(String.format("#%06X", 0xFFFFFF & getResources().getColor(R.color.board_default_color))); + binding.colorChooser.selectColor(String.format("#%06X", 0xFFFFFF & ContextCompat.getColor(requireContext(), R.color.board_default_color))); } return dialogBuilder diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/accesscontrol/AccessControlAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/accesscontrol/AccessControlAdapter.java index e5a50d9f4..0a1281fae 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/accesscontrol/AccessControlAdapter.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/accesscontrol/AccessControlAdapter.java @@ -10,6 +10,7 @@ import android.view.ViewGroup; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.appcompat.widget.SwitchCompat; +import androidx.core.content.ContextCompat; import androidx.core.graphics.drawable.DrawableCompat; import androidx.recyclerview.widget.RecyclerView; @@ -51,7 +52,7 @@ public class AccessControlAdapter extends RecyclerView.Adapter<RecyclerView.View this.account = account; this.accessControlChangedListener = accessControlChangedListener; this.context = context; - this.mainColor = context.getResources().getColor(R.color.primary); + this.mainColor = ContextCompat.getColor(context, R.color.primary); setHasStableIds(true); } @@ -172,9 +173,9 @@ public class AccessControlAdapter extends RecyclerView.Adapter<RecyclerView.View final int finalMainColor = getSecondaryForegroundColorDependingOnTheme(context, mainColor); DrawableCompat.setTintList(switchCompat.getThumbDrawable(), new ColorStateList( new int[][]{new int[]{android.R.attr.state_checked}, new int[]{}}, - new int[]{finalMainColor, context.getResources().getColor(R.color.fg_secondary)} + new int[]{finalMainColor, ContextCompat.getColor(context, R.color.fg_secondary)} )); - final int trackColor = context.getResources().getColor(R.color.fg_secondary); + final int trackColor = ContextCompat.getColor(context, R.color.fg_secondary); final int lightTrackColor = Color.argb(77, Color.red(trackColor), Color.green(trackColor), Color.blue(trackColor)); final int lightTrackColorChecked = Color.argb(77, Color.red(finalMainColor), Color.green(finalMainColor), Color.blue(finalMainColor)); DrawableCompat.setTintList(switchCompat.getTrackDrawable(), new ColorStateList( diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/accesscontrol/AccessControlDialogFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/accesscontrol/AccessControlDialogFragment.java index 33c0fe5b1..e2320ca45 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 @@ -111,7 +116,7 @@ public class AccessControlDialogFragment extends BrandedDialogFragment implement final WrappedLiveData<Void> wrappedDeleteLiveData = syncManager.deleteAccessControl(ac); adapter.remove(ac); observeOnce(wrappedDeleteLiveData, this, (ignored) -> { - if (wrappedDeleteLiveData.hasError()) { + if (wrappedDeleteLiveData.hasError() && !SyncManager.ignoreExceptionOnVoidError(wrappedDeleteLiveData.getError())) { DeckLog.logError(wrappedDeleteLiveData.getError()); BrandedSnackbar.make(requireView(), getString(R.string.error_revoking_ac, ac.getUser().getDisplayname()), Snackbar.LENGTH_LONG) .setAction(R.string.simple_more, v -> ExceptionDialogFragment.newInstance(wrappedDeleteLiveData.getError(), viewModel.getCurrentAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName())) @@ -129,7 +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/board/managelabels/EditLabelDialogFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/EditLabelDialogFragment.java index d460d1590..a0fb4cadd 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/EditLabelDialogFragment.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/EditLabelDialogFragment.java @@ -58,14 +58,14 @@ public class EditLabelDialogFragment extends BrandedDialogFragment { dialogBuilder.setTitle(getString(R.string.edit_tag, label.getTitle())); dialogBuilder.setPositiveButton(R.string.simple_save, (dialog, which) -> { - this.label.setColor(binding.colorChooser.getSelectedColor().substring(1)); + this.label.setColor(binding.colorChooser.getSelectedColor()); this.label.setTitle(binding.input.getText().toString()); listener.onLabelUpdated(this.label); }); String title = this.label.getTitle(); binding.input.setText(title); binding.input.setSelection(title.length()); - binding.colorChooser.selectColor("#" + this.label.getColor()); + binding.colorChooser.selectColor(String.format("#%06X", (0xFFFFFF & this.label.getColor()))); return dialogBuilder .setView(binding.getRoot()) diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/ManageLabelsDialogFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/ManageLabelsDialogFragment.java index 3391c7a99..e06606493 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/ManageLabelsDialogFragment.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/ManageLabelsDialogFragment.java @@ -80,7 +80,7 @@ public class ManageLabelsDialogFragment extends BrandedDialogFragment implements final Label label = new Label(); label.setBoardId(boardId); label.setTitle(binding.addLabelTitle.getText().toString()); - label.setColor(colors[new Random().nextInt(colors.length)].substring(1)); + label.setColor(colors[new Random().nextInt(colors.length)]); WrappedLiveData<Label> createLiveData = syncManager.createLabel(viewModel.getCurrentAccount().getId(), label, boardId); observeOnce(createLiveData, this, (createdLabel) -> { @@ -143,7 +143,7 @@ public class ManageLabelsDialogFragment extends BrandedDialogFragment implements private void deleteLabel(@NonNull Label label) { final WrappedLiveData<Void> deleteLiveData = syncManager.deleteLabel(label); observeOnce(deleteLiveData, this, (v) -> { - if (deleteLiveData.hasError()) { + if (deleteLiveData.hasError() && !SyncManager.ignoreExceptionOnVoidError(deleteLiveData.getError())) { final Throwable error = deleteLiveData.getError(); assert error != null; Toast.makeText(requireContext(), error.getLocalizedMessage(), Toast.LENGTH_LONG).show(); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/ManageLabelsViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/ManageLabelsViewHolder.java index 7fa3abd89..381a290e6 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/ManageLabelsViewHolder.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/managelabels/ManageLabelsViewHolder.java @@ -1,14 +1,13 @@ package it.niedermann.nextcloud.deck.ui.board.managelabels; import android.content.res.ColorStateList; -import android.graphics.Color; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; +import it.niedermann.android.util.ColorUtil; import it.niedermann.nextcloud.deck.databinding.ItemManageLabelBinding; import it.niedermann.nextcloud.deck.model.Label; -import it.niedermann.nextcloud.deck.util.ColorUtil; public class ManageLabelsViewHolder extends RecyclerView.ViewHolder { private ItemManageLabelBinding binding; @@ -17,13 +16,14 @@ public class ManageLabelsViewHolder extends RecyclerView.ViewHolder { public ManageLabelsViewHolder(ItemManageLabelBinding binding) { super(binding.getRoot()); this.binding = binding; + this.binding.label.setClickable(false); } public void bind(@NonNull Label label, @NonNull ManageLabelListener listener) { binding.label.setText(label.getTitle()); - final int labelColor = Color.parseColor("#" + label.getColor()); + final int labelColor = label.getColor(); binding.label.setChipBackgroundColor(ColorStateList.valueOf(labelColor)); - final int color = ColorUtil.getForegroundColorForBackgroundColor(labelColor); + final int color = ColorUtil.INSTANCE.getForegroundColorForBackgroundColor(labelColor); binding.label.setTextColor(color); binding.delete.setOnClickListener((v) -> listener.requestDelete(label)); binding.editText.setOnClickListener((v) -> listener.requestEdit(label)); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedDatePickerDialog.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedDatePickerDialog.java index 5bef66f2c..f429436a6 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedDatePickerDialog.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedDatePickerDialog.java @@ -17,7 +17,7 @@ import com.wdullaer.materialdatetimepicker.date.DatePickerDialog; import java.util.Calendar; import it.niedermann.nextcloud.deck.R; -import it.niedermann.nextcloud.deck.util.ColorUtil; +import it.niedermann.nextcloud.deck.util.DeckColorUtil; import static it.niedermann.nextcloud.deck.DeckApplication.isDarkTheme; import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.getSecondaryForegroundColorDependingOnTheme; @@ -44,7 +44,7 @@ public class BrandedDatePickerDialog extends DatePickerDialog implements Branded setOkColor(buttonTextColor); setCancelColor(buttonTextColor); // Text in picker title is always white - setAccentColor(ColorUtil.contrastRatioIsSufficientBigAreas(Color.WHITE, mainColor) ? mainColor : ContextCompat.getColor(requireContext(), R.color.accent)); + setAccentColor(DeckColorUtil.contrastRatioIsSufficientBigAreas(Color.WHITE, mainColor) ? mainColor : ContextCompat.getColor(requireContext(), R.color.accent)); } /** diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedDeleteAlertDialogBuilder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedDeleteAlertDialogBuilder.java index ec3cef553..d88fdd6cc 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedDeleteAlertDialogBuilder.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedDeleteAlertDialogBuilder.java @@ -5,6 +5,7 @@ import android.content.DialogInterface; import android.widget.Button; import androidx.annotation.CallSuper; +import androidx.core.content.ContextCompat; import it.niedermann.nextcloud.deck.R; @@ -20,7 +21,7 @@ public class BrandedDeleteAlertDialogBuilder extends BrandedAlertDialogBuilder { super.applyBrand(mainColor); final Button positiveButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE); if (positiveButton != null) { - positiveButton.setTextColor(getContext().getResources().getColor(R.color.danger)); + positiveButton.setTextColor(ContextCompat.getColor(getContext(), R.color.danger)); } } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedSnackbar.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedSnackbar.java index 20e6f8dc8..0159a59dc 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedSnackbar.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedSnackbar.java @@ -11,8 +11,8 @@ import androidx.core.content.ContextCompat; import com.google.android.material.snackbar.BaseTransientBottomBar; import com.google.android.material.snackbar.Snackbar; +import it.niedermann.android.util.ColorUtil; import it.niedermann.nextcloud.deck.R; -import it.niedermann.nextcloud.deck.util.ColorUtil; import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.isBrandingEnabled; import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.readBrandMainColor; @@ -25,9 +25,9 @@ public class BrandedSnackbar { final Snackbar snackbar = Snackbar.make(view, text, duration); if (isBrandingEnabled(view.getContext())) { @ColorInt final int color = readBrandMainColor(view.getContext()); - snackbar.setActionTextColor(ColorUtil.isColorDark(color) ? Color.WHITE : color); + snackbar.setActionTextColor(ColorUtil.INSTANCE.isColorDark(color) ? Color.WHITE : color); } else { - snackbar.setActionTextColor(ContextCompat.getColor(view.getContext(), R.color.primary)); + snackbar.setActionTextColor(ContextCompat.getColor(view.getContext(), R.color.defaultBrand)); } return snackbar; } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedTimePickerDialog.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedTimePickerDialog.java index a1963aa18..bd5e65ddd 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedTimePickerDialog.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandedTimePickerDialog.java @@ -17,7 +17,7 @@ import com.wdullaer.materialdatetimepicker.time.TimePickerDialog; import java.util.Calendar; import it.niedermann.nextcloud.deck.R; -import it.niedermann.nextcloud.deck.util.ColorUtil; +import it.niedermann.nextcloud.deck.util.DeckColorUtil; import static it.niedermann.nextcloud.deck.DeckApplication.isDarkTheme; import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.getSecondaryForegroundColorDependingOnTheme; @@ -44,7 +44,7 @@ public class BrandedTimePickerDialog extends TimePickerDialog implements Branded setOkColor(buttonTextColor); setCancelColor(buttonTextColor); // Text in picker title is always white - setAccentColor(ColorUtil.contrastRatioIsSufficientBigAreas(Color.WHITE, mainColor) ? mainColor : ContextCompat.getColor(requireContext(), R.color.accent)); + setAccentColor(DeckColorUtil.contrastRatioIsSufficientBigAreas(Color.WHITE, mainColor) ? mainColor : ContextCompat.getColor(requireContext(), R.color.accent)); } /** diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandingUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandingUtil.java index 49a8bce64..b780efec5 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandingUtil.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/branding/BrandingUtil.java @@ -17,14 +17,13 @@ import androidx.preference.PreferenceManager; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.tabs.TabLayout; +import it.niedermann.android.util.ColorUtil; import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.R; import static it.niedermann.nextcloud.deck.DeckApplication.isDarkTheme; -import static it.niedermann.nextcloud.deck.util.ColorUtil.contrastRatioIsSufficient; -import static it.niedermann.nextcloud.deck.util.ColorUtil.contrastRatioIsSufficientBigAreas; -import static it.niedermann.nextcloud.deck.util.ColorUtil.getContrastRatio; -import static it.niedermann.nextcloud.deck.util.ColorUtil.getForegroundColorForBackgroundColor; +import static it.niedermann.nextcloud.deck.util.DeckColorUtil.contrastRatioIsSufficient; +import static it.niedermann.nextcloud.deck.util.DeckColorUtil.contrastRatioIsSufficientBigAreas; public abstract class BrandingUtil { @@ -44,7 +43,7 @@ public abstract class BrandingUtil { DeckLog.log("--- Read: shared_preference_theme_main"); return sharedPreferences.getInt(context.getString(R.string.shared_preference_theme_main), context.getApplicationContext().getResources().getColor(R.color.defaultBrand)); } else { - return context.getResources().getColor(R.color.defaultBrand); + return ContextCompat.getColor(context, R.color.defaultBrand); } } @@ -87,13 +86,13 @@ public abstract class BrandingUtil { fab.setSupportBackgroundTintList(ColorStateList.valueOf(contrastRatioIsSufficient ? mainColor : ContextCompat.getColor(fab.getContext(), R.color.accent))); - fab.setColorFilter(contrastRatioIsSufficient ? getForegroundColorForBackgroundColor(mainColor) : mainColor); + fab.setColorFilter(contrastRatioIsSufficient ? ColorUtil.INSTANCE.getForegroundColorForBackgroundColor(mainColor) : mainColor); } public static void applyBrandToPrimaryTabLayout(@ColorInt int mainColor, @NonNull TabLayout tabLayout) { @ColorInt final int finalMainColor = getSecondaryForegroundColorDependingOnTheme(tabLayout.getContext(), mainColor); tabLayout.setBackgroundColor(ContextCompat.getColor(tabLayout.getContext(), R.color.primary)); - final boolean contrastRatioIsSufficient = getContrastRatio(mainColor, ContextCompat.getColor(tabLayout.getContext(), R.color.primary)) > 1.7d; + final boolean contrastRatioIsSufficient = ColorUtil.INSTANCE.getContrastRatio(mainColor, ContextCompat.getColor(tabLayout.getContext(), R.color.primary)) > 1.7d; tabLayout.setSelectedTabIndicatorColor(contrastRatioIsSufficient ? mainColor : finalMainColor); } @@ -112,7 +111,7 @@ public abstract class BrandingUtil { finalMainColor, finalMainColor, finalMainColor, - editText.getContext().getResources().getColor(R.color.fg_secondary) + ContextCompat.getColor(editText.getContext(), R.color.fg_secondary) } )); } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/AbstractCardViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/AbstractCardViewHolder.java new file mode 100644 index 000000000..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..ecb06c7a8 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,36 @@ 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.getLabels().size() == 0 + && fullCard.getCommentCount() == 0) { + return R.layout.item_card_default_only_title; + } + return R.layout.item_card_default; + } } - @SuppressLint("SetTextI18n") @Override - public void onBindViewHolder(@NonNull 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 +154,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 +162,7 @@ public class CardAdapter extends RecyclerView.Adapter<CardViewHolder> implements notifyItemInserted(position); } + @NonNull @Override public List<FullCard> getItemList() { return this.cardList; @@ -186,28 +213,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.getCard().getTitle(), fullCard.getLocalId()).show(fragmentManager, MoveCardDialogFragment.class.getSimpleName()); return true; } case R.id.action_card_archive: { @@ -222,7 +229,7 @@ public class CardAdapter extends RecyclerView.Adapter<CardViewHolder> implements case R.id.action_card_delete: { final WrappedLiveData<Void> deleteLiveData = syncManager.deleteCard(fullCard.getCard()); observeOnce(deleteLiveData, lifecycleOwner, (v) -> { - if (deleteLiveData.hasError()) { + if (deleteLiveData.hasError() && !SyncManager.ignoreExceptionOnVoidError(deleteLiveData.getError())) { ExceptionDialogFragment.newInstance(deleteLiveData.getError(), account).show(fragmentManager, ExceptionDialogFragment.class.getSimpleName()); } }); 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..6025cdada 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,18 @@ package it.niedermann.nextcloud.deck.ui.card; import android.content.Context; -import android.view.Menu; +import android.text.TextUtils; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnLongClickListener; +import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.ColorInt; import androidx.annotation.MenuRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.appcompat.widget.PopupMenu; -import androidx.core.graphics.drawable.DrawableCompat; -import androidx.recyclerview.widget.RecyclerView; +import androidx.core.content.ContextCompat; import com.google.android.material.card.MaterialCardView; @@ -22,21 +21,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.Card.TaskStatus; 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 +41,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,18 +52,7 @@ 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) { binding.cardCountAttachments.setVisibility(View.GONE); } else { @@ -79,7 +61,6 @@ public class CardViewHolder extends RecyclerView.ViewHolder { } final int commentsCount = fullCard.getCommentCount(); - if (commentsCount == 0) { binding.cardCountComments.setVisibility(View.GONE); } else { @@ -88,7 +69,7 @@ public class CardViewHolder extends RecyclerView.ViewHolder { binding.cardCountComments.setVisibility(View.VISIBLE); } - List<Label> labels = fullCard.getLabels(); + final List<Label> labels = fullCard.getLabels(); if (labels != null && labels.size() > 0) { binding.labels.updateLabels(labels); binding.labels.setVisibility(View.VISIBLE); @@ -97,30 +78,46 @@ public class CardViewHolder extends RecyclerView.ViewHolder { binding.labels.setVisibility(View.GONE); } - Card.TaskStatus taskStatus = fullCard.getCard().getTaskStatus(); + final TaskStatus taskStatus = fullCard.getCard().getTaskStatus(); if (taskStatus.taskCount > 0) { binding.cardCountTasks.setText(context.getResources().getString(R.string.task_count, String.valueOf(taskStatus.doneCount), String.valueOf(taskStatus.taskCount))); + binding.cardCountTasks.setCompoundDrawablesWithIntrinsicBounds(ContextCompat.getDrawable(context, R.drawable.ic_check_grey600_24dp), null, null, null); binding.cardCountTasks.setVisibility(View.VISIBLE); } else { - 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()); + final String description = fullCard.getCard().getDescription(); + if (!TextUtils.isEmpty(description)) { + binding.cardCountTasks.setCompoundDrawablesWithIntrinsicBounds(ContextCompat.getDrawable(context, R.drawable.ic_baseline_subject_24), null, null, null); + binding.cardCountTasks.setText(null); + binding.cardCountTasks.setVisibility(View.VISIBLE); } else { - menu.removeItem(menu.findItem(R.id.action_card_unassign).getItemId()); - } - if (boardRemoteId == null || fullCard.getCard().getId() == null) { - menu.removeItem(R.id.share_link); + binding.cardCountTasks.setVisibility(View.GONE); } + } + } + + @Override + protected TextView getCardDueDate() { + return binding.cardDueDate; + } - popup.setOnMenuItemClickListener(item -> optionsItemsSelectedListener.onCardOptionsItemSelected(item, fullCard)); - popup.show(); - }); + @Override + protected ImageView getNotSyncedYet() { + return binding.notSyncedYet; + } + + @Override + protected TextView getCardTitle() { + return binding.cardTitle; + } + + @Override + protected View getCardMenu() { + return binding.cardMenu; + } + + @Override + protected MaterialCardView getCard() { + return binding.card; } public void bindCardClickListener(@Nullable OnClickListener l) { @@ -135,11 +132,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/EditActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditActivity.java index 0710b7d7c..1af96bca5 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditActivity.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditActivity.java @@ -31,7 +31,6 @@ import it.niedermann.nextcloud.deck.ui.branding.BrandedAlertDialogBuilder; import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler; import it.niedermann.nextcloud.deck.util.CardUtil; -import static android.graphics.Color.parseColor; import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce; import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.applyBrandToPrimaryTabLayout; import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.isBrandingEnabled; @@ -83,7 +82,6 @@ public class EditActivity extends BrandedActivity { setSupportActionBar(binding.toolbar); viewModel = new ViewModelProvider(this).get(EditCardViewModel.class); - syncManager = new SyncManager(this); loadDataFromIntent(); } @@ -117,11 +115,12 @@ public class EditActivity extends BrandedActivity { throw new IllegalArgumentException(BUNDLE_KEY_ACCOUNT + " must not be null."); } viewModel.setAccount(account); + syncManager = new SyncManager(this, viewModel.getAccount().getName()); final long boardId = args.getLong(BUNDLE_KEY_BOARD_ID); observeOnce(syncManager.getFullBoardById(account.getId(), boardId), EditActivity.this, (fullBoard -> { - applyBrand(parseColor('#' + fullBoard.getBoard().getColor())); + applyBrand(fullBoard.getBoard().getColor()); viewModel.setCanEdit(fullBoard.getBoard().isPermissionEdit()); invalidateOptionsMenu(); if (viewModel.isCreateMode()) { @@ -138,7 +137,7 @@ public class EditActivity extends BrandedActivity { setupViewPager(); setupTitle(); } else { - observeOnce(syncManager.getCardByLocalId(account.getId(), cardId), EditActivity.this, (fullCard) -> { + observeOnce(syncManager.getFullCardWithProjectsByLocalId(account.getId(), cardId), EditActivity.this, (fullCard) -> { if (fullCard == null) { new BrandedAlertDialogBuilder(this) .setTitle(R.string.card_not_found) @@ -154,6 +153,8 @@ public class EditActivity extends BrandedActivity { }); } })); + + DeckLog.verbose("Finished loading intent data: { accountId = " + viewModel.getAccount().getId() + " , cardId = " + cardId + " }"); } @Override @@ -296,10 +297,10 @@ public class EditActivity extends BrandedActivity { @Override public void applyBrand(int mainColor) { - if(isBrandingEnabled(this)) { + if (isBrandingEnabled(this)) { final Drawable navigationIcon = binding.toolbar.getNavigationIcon(); if (navigationIcon == null) { - DeckLog.error("Excpected navigationIcon to be present."); + DeckLog.error("Expected navigationIcon to be present."); } else { DrawableCompat.setTint(binding.toolbar.getNavigationIcon(), colorAccent); } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditCardViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditCardViewModel.java index c8c93c838..62fe6785f 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditCardViewModel.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditCardViewModel.java @@ -8,15 +8,15 @@ import java.util.ArrayList; import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.model.Card; -import it.niedermann.nextcloud.deck.model.full.FullCard; +import it.niedermann.nextcloud.deck.model.full.FullCardWithProjects; @SuppressWarnings("WeakerAccess") public class EditCardViewModel extends ViewModel { private Account account; private long boardId; - private FullCard originalCard; - private FullCard fullCard; + private FullCardWithProjects originalCard; + private FullCardWithProjects fullCard; private boolean isSupportedVersion = false; private boolean hasCommentsAbility = false; private boolean pendingCreation = false; @@ -29,10 +29,10 @@ public class EditCardViewModel extends ViewModel { * @param boardId Local ID, expecting a positive long value * @param fullCard The card that is currently edited */ - public void initializeExistingCard(long boardId, @NonNull FullCard fullCard, boolean isSupportedVersion) { + public void initializeExistingCard(long boardId, @NonNull FullCardWithProjects fullCard, boolean isSupportedVersion) { this.boardId = boardId; this.fullCard = fullCard; - this.originalCard = new FullCard(this.fullCard); + this.originalCard = new FullCardWithProjects(this.fullCard); this.isSupportedVersion = isSupportedVersion; } @@ -43,7 +43,7 @@ public class EditCardViewModel extends ViewModel { * @param stackId Local ID, expecting a positive long value where the card should be created */ public void initializeNewCard(long boardId, long stackId, boolean isSupportedVersion) { - final FullCard fullCard = new FullCard(); + final FullCardWithProjects fullCard = new FullCardWithProjects(); fullCard.setLabels(new ArrayList<>()); fullCard.setAssignedUsers(new ArrayList<>()); fullCard.setAttachments(new ArrayList<>()); @@ -74,7 +74,7 @@ public class EditCardViewModel extends ViewModel { return account; } - public FullCard getFullCard() { + public FullCardWithProjects getFullCard() { return fullCard; } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/LabelAutoCompleteAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/LabelAutoCompleteAdapter.java index f77282e11..fe974f2a0 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/LabelAutoCompleteAdapter.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/LabelAutoCompleteAdapter.java @@ -9,6 +9,7 @@ import android.view.ViewGroup; import android.widget.Filter; import androidx.activity.ComponentActivity; +import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.graphics.drawable.DrawableCompat; @@ -18,11 +19,11 @@ import java.util.Collection; import java.util.List; import java.util.Random; +import it.niedermann.android.util.ColorUtil; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.ItemAutocompleteLabelBinding; import it.niedermann.nextcloud.deck.model.Label; import it.niedermann.nextcloud.deck.util.AutoCompleteAdapter; -import it.niedermann.nextcloud.deck.util.ColorUtil; import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce; @@ -35,7 +36,7 @@ public class LabelAutoCompleteAdapter extends AutoCompleteAdapter<Label> { public LabelAutoCompleteAdapter(@NonNull ComponentActivity activity, long accountId, long boardId, long cardId) { super(activity, accountId, boardId, cardId); final String[] colors = activity.getResources().getStringArray(R.array.board_default_colors); - final String createLabelColor = colors[new Random().nextInt(colors.length)].substring(1); + @ColorInt int createLabelColor = Color.parseColor(colors[new Random().nextInt(colors.length)]); observeOnce(syncManager.getFullBoardById(accountId, boardId), activity, (fullBoard) -> { if (fullBoard.getBoard().isPermissionManage()) { canManage = true; @@ -59,8 +60,8 @@ public class LabelAutoCompleteAdapter extends AutoCompleteAdapter<Label> { } final Label label = getItem(position); - final int labelColor = Color.parseColor("#" + label.getColor()); - final int color = ColorUtil.getForegroundColorForBackgroundColor(labelColor); + final int labelColor = label.getColor(); + final int color = ColorUtil.INSTANCE.getForegroundColorForBackgroundColor(labelColor); binding.label.setText(label.getTitle()); binding.label.setChipBackgroundColor(ColorStateList.valueOf(labelColor)); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/activities/CardActivityViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/activities/CardActivityViewHolder.java index 7d49b932d..c404f653e 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/activities/CardActivityViewHolder.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/activities/CardActivityViewHolder.java @@ -7,14 +7,13 @@ import android.view.View; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; +import it.niedermann.android.util.ClipboardUtil; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.ItemActivityBinding; import it.niedermann.nextcloud.deck.model.enums.ActivityType; import it.niedermann.nextcloud.deck.model.ocs.Activity; import it.niedermann.nextcloud.deck.util.DateUtil; -import static it.niedermann.nextcloud.deck.util.ClipboardUtil.copyToClipboard; - public class CardActivityViewHolder extends RecyclerView.ViewHolder { public ItemActivityBinding binding; @@ -31,7 +30,7 @@ public class CardActivityViewHolder extends RecyclerView.ViewHolder { itemView.setOnClickListener(View::showContextMenu); itemView.setOnCreateContextMenuListener((menu, v, menuInfo) -> { inflater.inflate(R.menu.activity_menu, menu); - menu.findItem(android.R.id.copy).setOnMenuItemClickListener(item -> copyToClipboard(context, activity.getSubject())); + menu.findItem(android.R.id.copy).setOnMenuItemClickListener(item -> ClipboardUtil.INSTANCE.copyToClipboard(context, activity.getSubject())); }); switch (ActivityType.findById(activity.getType())) { case CHANGE: @@ -55,7 +54,7 @@ public class CardActivityViewHolder extends RecyclerView.ViewHolder { case FILES: binding.type.setImageResource(R.drawable.type_file_36dp); case HISTORY: - binding.type.setImageResource(R.drawable.type_file_36dp); + binding.type.setImageResource(R.drawable.type_history_36dp); case DECK: default: break; diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/assignee/CardAssigneeDialog.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/assignee/CardAssigneeDialog.java new file mode 100644 index 000000000..74c89d830 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/assignee/CardAssigneeDialog.java @@ -0,0 +1,112 @@ +package it.niedermann.nextcloud.deck.ui.card.assignee; + +import android.app.Dialog; +import android.content.Context; +import android.graphics.Color; +import android.net.Uri; +import android.os.Bundle; +import android.view.LayoutInflater; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.DialogFragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.swiperefreshlayout.widget.CircularProgressDrawable; + +import com.bumptech.glide.Glide; + +import java.io.Serializable; + +import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.databinding.DialogAssigneeBinding; +import it.niedermann.nextcloud.deck.model.User; +import it.niedermann.nextcloud.deck.ui.branding.BrandedDeleteAlertDialogBuilder; +import it.niedermann.nextcloud.deck.ui.branding.BrandedDialogFragment; +import it.niedermann.nextcloud.deck.ui.card.EditCardViewModel; + +import static it.niedermann.nextcloud.deck.DeckApplication.isDarkTheme; + +public class CardAssigneeDialog extends BrandedDialogFragment { + + private static final String KEY_USER = "user"; + private DialogAssigneeBinding binding; + private EditCardViewModel viewModel; + + @Nullable + private CardAssigneeListener cardAssigneeListener = null; + @SuppressWarnings("NotNullFieldNotInitialized") + @NonNull + private User user; + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + + if (getParentFragment() instanceof CardAssigneeListener) { + this.cardAssigneeListener = (CardAssigneeListener) getParentFragment(); + } else if (context instanceof CardAssigneeListener) { + this.cardAssigneeListener = (CardAssigneeListener) context; + } + + final Bundle args = requireArguments(); + if (!args.containsKey(KEY_USER)) { + throw new IllegalArgumentException("Provide at least " + KEY_USER); + } + final Serializable user = args.getSerializable(KEY_USER); + if (user == null) { + throw new IllegalArgumentException(KEY_USER + " must not be null."); + } + this.user = (User) user; + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + binding = DialogAssigneeBinding.inflate(LayoutInflater.from(requireContext())); + viewModel = new ViewModelProvider(requireActivity()).get(EditCardViewModel.class); + + AlertDialog.Builder dialogBuilder = new BrandedDeleteAlertDialogBuilder(requireContext()); + + if (viewModel.canEdit() && cardAssigneeListener != null) { + dialogBuilder.setPositiveButton(R.string.simple_unassign, (d, w) -> cardAssigneeListener.onUnassignUser(user)); + } + + return dialogBuilder + .setView(binding.getRoot()) + .setNeutralButton(R.string.simple_close, null) + .create(); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + final Context context = requireContext(); + + final CircularProgressDrawable circularProgressDrawable = new CircularProgressDrawable(context); + circularProgressDrawable.setStrokeWidth(5f); + circularProgressDrawable.setCenterRadius(30f); + circularProgressDrawable.setColorSchemeColors(isDarkTheme(context) ? Color.LTGRAY : Color.DKGRAY); + circularProgressDrawable.start(); + + binding.avatar.post(() -> Glide.with(binding.avatar.getContext()) + .load(viewModel.getAccount().getUrl() + "/index.php/avatar/" + Uri.encode(user.getUid()) + "/" + binding.avatar.getWidth()) + .placeholder(circularProgressDrawable) + .error(R.drawable.ic_person_grey600_24dp) + .into(binding.avatar)); + binding.displayName.setText(user.getDisplayname()); + } + + @Override + public void applyBrand(int mainColor) { + } + + public static DialogFragment newInstance(@NonNull User user) { + final DialogFragment fragment = new CardAssigneeDialog(); + final Bundle args = new Bundle(); + args.putSerializable(KEY_USER, user); + fragment.setArguments(args); + return fragment; + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/assignee/CardAssigneeListener.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/assignee/CardAssigneeListener.java new file mode 100644 index 000000000..259a8b57c --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/assignee/CardAssigneeListener.java @@ -0,0 +1,11 @@ +package it.niedermann.nextcloud.deck.ui.card.assignee; + +import androidx.annotation.NonNull; + +import it.niedermann.nextcloud.deck.model.User; + +public interface CardAssigneeListener { + + void onUnassignUser(@NonNull User user); + +}
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/AttachmentDeletedListener.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/AttachmentDeletedListener.java index c236fa4c5..2d3ece255 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/AttachmentDeletedListener.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/AttachmentDeletedListener.java @@ -3,5 +3,5 @@ package it.niedermann.nextcloud.deck.ui.card.attachments; import it.niedermann.nextcloud.deck.model.Attachment; public interface AttachmentDeletedListener { - void onAttachmentDeleted(Attachment attachment); - }
\ No newline at end of file + void onAttachmentDeleted(Attachment attachment); +}
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/AttachmentViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/AttachmentViewHolder.java index ed3031b7c..533bbe322 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/AttachmentViewHolder.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/AttachmentViewHolder.java @@ -1,18 +1,59 @@ package it.niedermann.nextcloud.deck.ui.card.attachments; +import android.view.MenuInflater; import android.view.View; import android.widget.ImageView; +import androidx.annotation.CallSuper; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.graphics.drawable.DrawableCompat; +import androidx.fragment.app.FragmentManager; import androidx.recyclerview.widget.RecyclerView; +import it.niedermann.android.util.ClipboardUtil; +import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.model.Attachment; +import it.niedermann.nextcloud.deck.model.enums.DBStatus; +import it.niedermann.nextcloud.deck.ui.branding.BrandingUtil; +import it.niedermann.nextcloud.deck.util.AttachmentUtil; + public abstract class AttachmentViewHolder extends RecyclerView.ViewHolder { AttachmentViewHolder(@NonNull View itemView) { super(itemView); } + public void bind(@NonNull Account account, @NonNull MenuInflater menuInflater, @NonNull FragmentManager fragmentManager, Long cardRemoteId, Attachment attachment, @Nullable View.OnClickListener onClickListener, @ColorInt int mainColor) { + bind(menuInflater, fragmentManager, cardRemoteId, attachment, onClickListener, mainColor, AttachmentUtil.getRemoteOrLocalUrl(account.getUrl(), cardRemoteId, attachment)); + } + + @CallSuper + public void bind(@NonNull MenuInflater menuInflater, @NonNull FragmentManager fragmentManager, Long cardRemoteId, Attachment attachment, @Nullable View.OnClickListener onClickListener, @ColorInt int mainColor, @Nullable String attachmentUri) { + setNotSyncedYetStatus(!DBStatus.LOCAL_EDITED.equals(attachment.getStatusEnum()), mainColor); + itemView.setOnCreateContextMenuListener((menu, v, menuInfo) -> { + menuInflater.inflate(R.menu.attachment_menu, menu); + menu.findItem(R.id.delete).setOnMenuItemClickListener(item -> { + DeleteAttachmentDialogFragment.newInstance(attachment).show(fragmentManager, DeleteAttachmentDialogFragment.class.getCanonicalName()); + return false; + }); + if (attachmentUri == null || attachment.getId() == null || cardRemoteId == null) { + menu.findItem(android.R.id.copyUrl).setVisible(false); + } else { + menu.findItem(android.R.id.copyUrl).setVisible(true); + menu.findItem(android.R.id.copyUrl).setOnMenuItemClickListener(item -> ClipboardUtil.INSTANCE.copyToClipboard(itemView.getContext(), attachment.getFilename(), attachmentUri)); + } + }); + } + abstract protected ImageView getPreview(); - abstract protected void setNotSyncedYetStatus(boolean synced, @ColorInt int color); + protected void setNotSyncedYetStatus(boolean synced, @ColorInt int mainColor) { + final ImageView notSyncedYet = getNotSyncedYetStatusIcon(); + DrawableCompat.setTint(notSyncedYet.getDrawable(), BrandingUtil.getSecondaryForegroundColorDependingOnTheme(notSyncedYet.getContext(), mainColor)); + notSyncedYet.setVisibility(synced ? View.GONE : View.VISIBLE); + } + + abstract protected ImageView getNotSyncedYetStatusIcon(); }
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentAdapter.java index a72ed8ef2..ea347417a 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentAdapter.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentAdapter.java @@ -3,9 +3,7 @@ package it.niedermann.nextcloud.deck.ui.card.attachments; import android.app.Activity; import android.content.Context; import android.content.Intent; -import android.net.Uri; import android.os.Build; -import android.text.format.Formatter; import android.view.LayoutInflater; import android.view.MenuInflater; import android.view.View; @@ -18,8 +16,6 @@ import androidx.core.app.ActivityOptionsCompat; import androidx.fragment.app.FragmentManager; import androidx.recyclerview.widget.RecyclerView; -import com.bumptech.glide.Glide; - import java.util.ArrayList; import java.util.List; @@ -28,17 +24,15 @@ import it.niedermann.nextcloud.deck.databinding.ItemAttachmentDefaultBinding; import it.niedermann.nextcloud.deck.databinding.ItemAttachmentImageBinding; import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.model.Attachment; -import it.niedermann.nextcloud.deck.model.enums.DBStatus; import it.niedermann.nextcloud.deck.ui.attachments.AttachmentsActivity; -import it.niedermann.nextcloud.deck.util.AttachmentUtil; -import it.niedermann.nextcloud.deck.util.DateUtil; +import it.niedermann.nextcloud.deck.ui.branding.Branded; import it.niedermann.nextcloud.deck.util.MimeTypeUtil; import static androidx.recyclerview.widget.RecyclerView.NO_ID; -import static it.niedermann.nextcloud.deck.util.ClipboardUtil.copyToClipboard; +import static it.niedermann.nextcloud.deck.util.AttachmentUtil.openAttachmentInBrowser; @SuppressWarnings("WeakerAccess") -public class CardAttachmentAdapter extends RecyclerView.Adapter<AttachmentViewHolder> { +public class CardAttachmentAdapter extends RecyclerView.Adapter<AttachmentViewHolder> implements Branded { public static final int VIEW_TYPE_DEFAULT = 2; public static final int VIEW_TYPE_IMAGE = 1; @@ -54,14 +48,13 @@ public class CardAttachmentAdapter extends RecyclerView.Adapter<AttachmentViewHo FragmentManager fragmentManager; @NonNull private List<Attachment> attachments = new ArrayList<>(); - @Nullable + @NonNull private final AttachmentClickedListener attachmentClickedListener; CardAttachmentAdapter( - @NonNull Context context, @NonNull FragmentManager fragmentManager, @NonNull MenuInflater menuInflater, - @Nullable AttachmentClickedListener attachmentClickedListener, + @NonNull AttachmentClickedListener attachmentClickedListener, @NonNull Account account, @Nullable Long cardLocalId ) { @@ -95,39 +88,14 @@ public class CardAttachmentAdapter extends RecyclerView.Adapter<AttachmentViewHo @Override public void onBindViewHolder(@NonNull AttachmentViewHolder holder, int position) { - final Context context = holder.itemView.getContext(); final Attachment attachment = attachments.get(position); - final int viewType = getItemViewType(position); - - @Nullable final String uri = (attachment.getId() == null || cardRemoteId == null) - ? attachment.getLocalPath() : - AttachmentUtil.getRemoteUrl(account.getUrl(), cardRemoteId, attachment.getId()); - holder.setNotSyncedYetStatus(!DBStatus.LOCAL_EDITED.equals(attachment.getStatusEnum()), mainColor); - holder.itemView.setOnCreateContextMenuListener((menu, v, menuInfo) -> { - menuInflater.inflate(R.menu.attachment_menu, menu); - menu.findItem(R.id.delete).setOnMenuItemClickListener(item -> { - DeleteAttachmentDialogFragment.newInstance(attachment).show(fragmentManager, DeleteAttachmentDialogFragment.class.getCanonicalName()); - return false; - }); - if (uri == null) { - menu.findItem(android.R.id.copyUrl).setVisible(false); - } else { - menu.findItem(android.R.id.copyUrl).setOnMenuItemClickListener(item -> copyToClipboard(context, attachment.getFilename(), uri)); - } - }); + final Context context = holder.itemView.getContext(); + final View.OnClickListener onClickListener; - switch (viewType) { + switch (getItemViewType(position)) { case VIEW_TYPE_IMAGE: { - holder.getPreview().setImageResource(R.drawable.ic_image_grey600_24dp); - Glide.with(context) - .load(uri) - .placeholder(R.drawable.ic_image_grey600_24dp) - .error(R.drawable.ic_image_grey600_24dp) - .into(holder.getPreview()); - holder.itemView.setOnClickListener((v) -> { - if (attachmentClickedListener != null) { - attachmentClickedListener.onAttachmentClicked(position); - } + onClickListener = (event) -> { + attachmentClickedListener.onAttachmentClicked(position); final Intent intent = AttachmentsActivity.createIntent(context, account, cardLocalId, attachment.getLocalId()); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && context instanceof Activity) { String transitionName = context.getString(R.string.transition_attachment_preview, String.valueOf(attachment.getLocalId())); @@ -136,46 +104,16 @@ public class CardAttachmentAdapter extends RecyclerView.Adapter<AttachmentViewHo } else { context.startActivity(intent); } - }); + }; break; } case VIEW_TYPE_DEFAULT: default: { - DefaultAttachmentViewHolder defaultHolder = (DefaultAttachmentViewHolder) holder; - - if (MimeTypeUtil.isAudio(attachment.getMimetype())) { - holder.getPreview().setImageResource(R.drawable.ic_music_note_grey600_24dp); - } else if (MimeTypeUtil.isVideo(attachment.getMimetype())) { - holder.getPreview().setImageResource(R.drawable.ic_local_movies_grey600_24dp); - } else if (MimeTypeUtil.isPdf(attachment.getMimetype())) { - holder.getPreview().setImageResource(R.drawable.ic_baseline_picture_as_pdf_24); - } else if (MimeTypeUtil.isContact(attachment.getMimetype())) { - holder.getPreview().setImageResource(R.drawable.ic_baseline_contact_mail_24); - } else { - holder.getPreview().setImageResource(R.drawable.ic_attach_file_grey600_24dp); - } - - if (cardRemoteId != null) { - defaultHolder.itemView.setOnClickListener((event) -> { - Intent openURL = new Intent(Intent.ACTION_VIEW); - openURL.setData(Uri.parse(AttachmentUtil.getRemoteUrl(account.getUrl(), cardRemoteId, attachment.getId()))); - context.startActivity(openURL); - }); - } - defaultHolder.binding.filename.setText(attachment.getBasename()); - defaultHolder.binding.filesize.setText(Formatter.formatFileSize(context, attachment.getFilesize())); - if (attachment.getLastModifiedLocal() != null) { - defaultHolder.binding.modified.setText(DateUtil.getRelativeDateTimeString(context, attachment.getLastModifiedLocal().getTime())); - defaultHolder.binding.modified.setVisibility(View.VISIBLE); - } else if (attachment.getLastModified() != null) { - defaultHolder.binding.modified.setText(DateUtil.getRelativeDateTimeString(context, attachment.getLastModified().getTime())); - defaultHolder.binding.modified.setVisibility(View.VISIBLE); - } else { - defaultHolder.binding.modified.setVisibility(View.GONE); - } + onClickListener = (event) -> openAttachmentInBrowser(context, account.getUrl(), cardRemoteId, attachment.getId()); break; } } + holder.bind(account, menuInflater, fragmentManager, cardRemoteId, attachment, onClickListener, mainColor); } @Override @@ -205,4 +143,10 @@ public class CardAttachmentAdapter extends RecyclerView.Adapter<AttachmentViewHo this.attachments.remove(a); notifyItemRemoved(index); } + + @Override + public void applyBrand(@ColorInt int mainColor) { + this.mainColor = mainColor; + notifyDataSetChanged(); + } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentsFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/CardAttachmentsFragment.java index 95cbb3d5e..dd835b598 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,11 +1,11 @@ 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.database.Cursor; import android.net.Uri; -import android.os.Build; import android.os.Bundle; import android.provider.ContactsContract; import android.provider.MediaStore; @@ -14,6 +14,7 @@ import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -55,6 +56,13 @@ import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment; import static android.app.Activity.RESULT_OK; import static androidx.core.content.PermissionChecker.checkSelfPermission; +import static android.Manifest.permission.READ_EXTERNAL_STORAGE; +import static android.app.Activity.RESULT_OK; +import static android.os.Build.VERSION.SDK_INT; +import static android.os.Build.VERSION_CODES.KITKAT; +import static android.os.Build.VERSION_CODES.M; +import static androidx.core.content.PermissionChecker.PERMISSION_GRANTED; +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; @@ -95,7 +103,6 @@ public class CardAttachmentsFragment extends BrandedFragment implements Attachme syncManager = new SyncManager(requireContext()); adapter = new CardAttachmentAdapter( - requireContext(), getChildFragmentManager(), requireActivity().getMenuInflater(), this, @@ -326,10 +333,11 @@ public class CardAttachmentsFragment extends BrandedFragment implements Attachme public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { switch (requestCode) { case REQUEST_CODE_ADD_FILE_PERMISSION: - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + if (SDK_INT >= KITKAT && checkSelfPermission(requireActivity(), READ_EXTERNAL_STORAGE) == PERMISSION_GRANTED) { pickFile(); + } else { + Toast.makeText(requireContext(), R.string.cannot_upload_files_without_permission, Toast.LENGTH_LONG).show(); } - break; case REQUEST_CODE_PICK_CONTACT_PERMISSION: pickContact(); break; @@ -343,7 +351,12 @@ public class CardAttachmentsFragment extends BrandedFragment implements Attachme 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() && !SyncManager.ignoreExceptionOnVoidError(deleteLiveData.getError())) { + ExceptionDialogFragment.newInstance(deleteLiveData.getError(), viewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + } + }); } updateEmptyContentView(); } @@ -366,6 +379,7 @@ public class CardAttachmentsFragment extends BrandedFragment implements Attachme @Override public void applyBrand(int mainColor) { applyBrandToFAB(mainColor, binding.fab); + adapter.applyBrand(mainColor); } public static Fragment newInstance() { diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/DefaultAttachmentViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/DefaultAttachmentViewHolder.java index 7acdd390e..2890d2a81 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/DefaultAttachmentViewHolder.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/DefaultAttachmentViewHolder.java @@ -1,15 +1,25 @@ package it.niedermann.nextcloud.deck.ui.card.attachments; +import android.text.format.Formatter; +import android.view.MenuInflater; import android.view.View; import android.widget.ImageView; import androidx.annotation.ColorInt; -import androidx.core.graphics.drawable.DrawableCompat; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.FragmentManager; +import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.ItemAttachmentDefaultBinding; +import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.model.Attachment; +import it.niedermann.nextcloud.deck.util.AttachmentUtil; +import it.niedermann.nextcloud.deck.util.DateUtil; +import it.niedermann.nextcloud.deck.util.MimeTypeUtil; public class DefaultAttachmentViewHolder extends AttachmentViewHolder { - ItemAttachmentDefaultBinding binding; + private ItemAttachmentDefaultBinding binding; @SuppressWarnings("WeakerAccess") public DefaultAttachmentViewHolder(ItemAttachmentDefaultBinding binding) { @@ -23,8 +33,36 @@ public class DefaultAttachmentViewHolder extends AttachmentViewHolder { } @Override - protected void setNotSyncedYetStatus(boolean synced, @ColorInt int mainColor) { - DrawableCompat.setTint(binding.notSyncedYet.getDrawable(), mainColor); - binding.notSyncedYet.setVisibility(synced ? View.GONE : View.VISIBLE); + protected ImageView getNotSyncedYetStatusIcon() { + return binding.notSyncedYet; + } + + public void bind(@NonNull Account account, @NonNull MenuInflater menuInflater, @NonNull FragmentManager fragmentManager, Long cardRemoteId, Attachment attachment, @Nullable View.OnClickListener onClickListener, @ColorInt int mainColor) { + super.bind(account, menuInflater, fragmentManager, cardRemoteId, attachment, onClickListener, mainColor); + + if (MimeTypeUtil.isAudio(attachment.getMimetype())) { + getPreview().setImageResource(R.drawable.ic_music_note_grey600_24dp); + } else if (MimeTypeUtil.isVideo(attachment.getMimetype())) { + getPreview().setImageResource(R.drawable.ic_local_movies_grey600_24dp); + } else if (MimeTypeUtil.isPdf(attachment.getMimetype())) { + getPreview().setImageResource(R.drawable.ic_baseline_picture_as_pdf_24); + } else if (MimeTypeUtil.isContact(attachment.getMimetype())) { + getPreview().setImageResource(R.drawable.ic_baseline_contact_mail_24); + } else { + getPreview().setImageResource(R.drawable.ic_attach_file_grey600_24dp); + } + + itemView.setOnClickListener((event) -> AttachmentUtil.openAttachmentInBrowser(itemView.getContext(), account.getUrl(), cardRemoteId, attachment.getId())); + binding.filename.setText(attachment.getBasename()); + binding.filesize.setText(Formatter.formatFileSize(binding.filesize.getContext(), attachment.getFilesize())); + if (attachment.getLastModifiedLocal() != null) { + binding.modified.setText(DateUtil.getRelativeDateTimeString(binding.modified.getContext(), attachment.getLastModifiedLocal().getTime())); + binding.modified.setVisibility(View.VISIBLE); + } else if (attachment.getLastModified() != null) { + binding.modified.setText(DateUtil.getRelativeDateTimeString(binding.modified.getContext(), attachment.getLastModified().getTime())); + binding.modified.setVisibility(View.VISIBLE); + } else { + binding.modified.setVisibility(View.GONE); + } } }
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/ImageAttachmentViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/ImageAttachmentViewHolder.java index d13675a30..e3139295f 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/ImageAttachmentViewHolder.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/ImageAttachmentViewHolder.java @@ -1,12 +1,21 @@ package it.niedermann.nextcloud.deck.ui.card.attachments; +import android.view.MenuInflater; import android.view.View; import android.widget.ImageView; import androidx.annotation.ColorInt; -import androidx.core.graphics.drawable.DrawableCompat; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.FragmentManager; +import com.bumptech.glide.Glide; + +import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.ItemAttachmentImageBinding; +import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.model.Attachment; +import it.niedermann.nextcloud.deck.util.AttachmentUtil; public class ImageAttachmentViewHolder extends AttachmentViewHolder { private ItemAttachmentImageBinding binding; @@ -23,8 +32,21 @@ public class ImageAttachmentViewHolder extends AttachmentViewHolder { } @Override - protected void setNotSyncedYetStatus(boolean synced, @ColorInt int mainColor) { - DrawableCompat.setTint(binding.notSyncedYet.getDrawable(), mainColor); - binding.notSyncedYet.setVisibility(synced ? View.GONE : View.VISIBLE); + protected ImageView getNotSyncedYetStatusIcon() { + return binding.notSyncedYet; + } + + public void bind(@NonNull Account account, @NonNull MenuInflater menuInflater, @NonNull FragmentManager fragmentManager, Long cardRemoteId, Attachment attachment, @Nullable View.OnClickListener onClickListener, @ColorInt int mainColor) { + @Nullable final String uri = AttachmentUtil.getRemoteOrLocalUrl(account.getUrl(), cardRemoteId, attachment); + + super.bind(menuInflater, fragmentManager, cardRemoteId, attachment, onClickListener, mainColor, uri); + + getPreview().setImageResource(R.drawable.ic_image_grey600_24dp); + Glide.with(getPreview().getContext()) + .load(uri) + .placeholder(R.drawable.ic_image_grey600_24dp) + .error(R.drawable.ic_image_grey600_24dp) + .into(getPreview()); + itemView.setOnClickListener(onClickListener); } }
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsFragment.java index e261c37a2..2090e7ad8 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsFragment.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsFragment.java @@ -23,12 +23,15 @@ import it.niedermann.nextcloud.deck.databinding.FragmentCardEditTabCommentsBindi import it.niedermann.nextcloud.deck.model.ocs.comment.DeckComment; import it.niedermann.nextcloud.deck.model.ocs.comment.full.FullDeckComment; import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; +import it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.WrappedLiveData; import it.niedermann.nextcloud.deck.ui.branding.BrandedFragment; import it.niedermann.nextcloud.deck.ui.card.EditActivity; import it.niedermann.nextcloud.deck.ui.card.EditCardViewModel; +import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment; import static android.view.View.GONE; import static android.view.View.VISIBLE; +import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce; import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.applyBrandToEditText; import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.applyBrandToFAB; import static it.niedermann.nextcloud.deck.util.ViewUtil.setupMentions; @@ -116,6 +119,7 @@ public class CardCommentsFragment extends BrandedFragment implements CommentEdit } return true; }); + binding.message.addTextChangedListener(new CardCommentsMentionProposer(getViewLifecycleOwner(), mainViewModel.getAccount(), mainViewModel.getBoardId(), binding.message, binding.mentionProposerWrapper, binding.mentionProposer)); } else { binding.addCommentLayout.setVisibility(GONE); } @@ -138,7 +142,12 @@ public class CardCommentsFragment extends BrandedFragment implements CommentEdit @Override public void onCommentDeleted(Long localId) { - syncManager.deleteComment(mainViewModel.getAccount().getId(), mainViewModel.getFullCard().getLocalId(), localId); + final WrappedLiveData<Void> deleteLiveData = syncManager.deleteComment(mainViewModel.getAccount().getId(), mainViewModel.getFullCard().getLocalId(), localId); + observeOnce(deleteLiveData, this, (next) -> { + if (deleteLiveData.hasError() && !SyncManager.ignoreExceptionOnVoidError(deleteLiveData.getError())) { + ExceptionDialogFragment.newInstance(deleteLiveData.getError(), mainViewModel.getAccount()).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + } + }); } @Override diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsMentionProposer.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsMentionProposer.java new file mode 100644 index 000000000..7ca7a6384 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsMentionProposer.java @@ -0,0 +1,139 @@ +package it.niedermann.nextcloud.deck.ui.card.comments; + +import android.annotation.SuppressLint; +import android.net.Uri; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.View; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.LinearLayout; + +import androidx.annotation.NonNull; +import androidx.core.util.Pair; +import androidx.lifecycle.LifecycleOwner; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.RequestOptions; + +import java.util.ArrayList; +import java.util.List; + +import it.niedermann.android.util.DimensionUtil; +import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.model.User; +import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; +import it.niedermann.nextcloud.deck.ui.card.comments.util.CommentsUtil; + +import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce; + +public class CardCommentsMentionProposer implements TextWatcher { + + private final int avatarSize; + @NonNull + private final SyncManager syncManager; + @NonNull + private final LinearLayout.LayoutParams layoutParams; + @NonNull + private final LifecycleOwner owner; + @NonNull + private final Account account; + private final long boardLocalId; + @NonNull + private final EditText editText; + @NonNull + private final LinearLayout mentionProposer; + @NonNull + private final LinearLayout mentionProposerWrapper; + + @NonNull + private final List<User> users = new ArrayList<>(); + + public CardCommentsMentionProposer(@NonNull LifecycleOwner owner, @NonNull Account account, long boardLocalId, @NonNull EditText editText, LinearLayout mentionProposerWrapper, @NonNull LinearLayout avatarProposer) { + this.owner = owner; + this.account = account; + this.boardLocalId = boardLocalId; + this.editText = editText; + this.mentionProposerWrapper = mentionProposerWrapper; + this.mentionProposer = avatarProposer; + syncManager = new SyncManager(editText.getContext()); + avatarSize = DimensionUtil.INSTANCE.dpToPx(mentionProposer.getContext(), R.dimen.avatar_size_small); + layoutParams = new LinearLayout.LayoutParams(avatarSize, avatarSize); + layoutParams.setMarginEnd(DimensionUtil.INSTANCE.dpToPx(mentionProposer.getContext(), R.dimen.spacer_1x)); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + final int selectionStart = editText.getSelectionStart(); + final int selectionEnd = editText.getSelectionEnd(); + final Pair<String, Integer> mentionProposal = CommentsUtil.getUserNameForMentionProposal(s.toString(), selectionStart); + if (mentionProposal == null || (mentionProposal.first != null && mentionProposal.first.length() == 0) || selectionStart != selectionEnd) { + mentionProposer.removeAllViews(); + mentionProposerWrapper.setVisibility(View.GONE); + this.users.clear(); + } else { + if (mentionProposal.first != null && mentionProposal.second != null) { + observeOnce(syncManager.searchUserByUidOrDisplayName(account.getId(), boardLocalId, -1L, mentionProposal.first), owner, (users) -> { + if (!users.equals(this.users)) { + mentionProposer.removeAllViews(); + if (users.size() > 0) { + mentionProposerWrapper.setVisibility(View.VISIBLE); + for (User user : users) { + final ImageView avatar = new ImageView(mentionProposer.getContext()); + avatar.setLayoutParams(layoutParams); + updateListenerOfView(avatar, s, mentionProposal, user); + + mentionProposer.addView(avatar); + + Glide.with(avatar.getContext()) + .load(account.getUrl() + "/index.php/avatar/" + Uri.encode(user.getUid()) + "/" + avatarSize) + .placeholder(R.drawable.ic_person_grey600_24dp) + .error(R.drawable.ic_person_grey600_24dp) + .apply(RequestOptions.circleCropTransform()) + .into(avatar); + } + } else { + mentionProposerWrapper.setVisibility(View.GONE); + } + this.users.clear(); + this.users.addAll(users); + } else { + int i = 0; + for (User user : users) { + updateListenerOfView(mentionProposer.getChildAt(i), s, mentionProposal, user); + i++; + } + } + }); + } else { + this.users.clear(); + mentionProposer.removeAllViews(); + mentionProposerWrapper.setVisibility(View.GONE); + } + } + } + + @SuppressLint("SetTextI18n") + private void updateListenerOfView(View avatar, CharSequence s, Pair<String, Integer> mentionProposal, User user) { + avatar.setOnClickListener((c) -> { + editText.setText( + s.subSequence(0, mentionProposal.second) + + user.getUid() + + s.subSequence(mentionProposal.second + mentionProposal.first.length(), s.length()) + ); + editText.setSelection(mentionProposal.second + user.getUid().length()); + mentionProposerWrapper.setVisibility(View.GONE); + }); + } + + @Override + public void afterTextChanged(Editable s) { + + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/ItemCommentViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/ItemCommentViewHolder.java index 086d799af..c4cde19f0 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/ItemCommentViewHolder.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/ItemCommentViewHolder.java @@ -13,16 +13,16 @@ import androidx.recyclerview.widget.RecyclerView; import java.text.DateFormat; +import it.niedermann.android.util.ClipboardUtil; +import it.niedermann.android.util.DimensionUtil; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.ItemCommentBinding; import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.model.enums.DBStatus; import it.niedermann.nextcloud.deck.model.ocs.comment.full.FullDeckComment; import it.niedermann.nextcloud.deck.util.DateUtil; -import it.niedermann.nextcloud.deck.util.DimensionUtil; import it.niedermann.nextcloud.deck.util.ViewUtil; -import static it.niedermann.nextcloud.deck.util.ClipboardUtil.copyToClipboard; import static it.niedermann.nextcloud.deck.util.ViewUtil.setupMentions; public class ItemCommentViewHolder extends RecyclerView.ViewHolder { @@ -35,7 +35,7 @@ public class ItemCommentViewHolder extends RecyclerView.ViewHolder { } public void bind(@NonNull FullDeckComment comment, @NonNull Account account, @ColorInt int mainColor, @NonNull MenuInflater inflater, @NonNull CommentDeletedListener deletedListener, @NonNull CommentSelectAsReplyListener selectAsReplyListener, @NonNull FragmentManager fragmentManager) { - ViewUtil.addAvatar(binding.avatar, account.getUrl(), comment.getComment().getActorId(), DimensionUtil.dpToPx(binding.avatar.getContext(), R.dimen.icon_size_details), R.drawable.ic_person_grey600_24dp); + ViewUtil.addAvatar(binding.avatar, account.getUrl(), comment.getComment().getActorId(), DimensionUtil.INSTANCE.dpToPx(binding.avatar.getContext(), R.dimen.icon_size_details), R.drawable.ic_person_grey600_24dp); binding.message.setText(comment.getComment().getMessage()); binding.actorDisplayName.setText(comment.getComment().getActorDisplayName()); binding.creationDateTime.setText(DateUtil.getRelativeDateTimeString(binding.creationDateTime.getContext(), comment.getComment().getCreationDateTime().getTime())); @@ -43,7 +43,7 @@ public class ItemCommentViewHolder extends RecyclerView.ViewHolder { itemView.setOnCreateContextMenuListener((menu, v, menuInfo) -> { inflater.inflate(R.menu.comment_menu, menu); - menu.findItem(android.R.id.copy).setOnMenuItemClickListener(item -> copyToClipboard(itemView.getContext(), comment.getComment().getMessage())); + menu.findItem(android.R.id.copy).setOnMenuItemClickListener(item -> ClipboardUtil.INSTANCE.copyToClipboard(itemView.getContext(), comment.getComment().getMessage())); final MenuItem replyMenuItem = menu.findItem(R.id.reply); if (comment.getStatusEnum() != DBStatus.LOCAL_EDITED && account.getServerDeckVersionAsObject().supportsCommentsReplys()) { replyMenuItem.setOnMenuItemClickListener(item -> { diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/util/CommentsUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/util/CommentsUtil.java new file mode 100644 index 000000000..5251291c8 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/util/CommentsUtil.java @@ -0,0 +1,48 @@ +package it.niedermann.nextcloud.deck.ui.card.comments.util; + + +import androidx.core.util.Pair; + +public class CommentsUtil { + + public static Pair<String, Integer> getUserNameForMentionProposal(String text, int cursorPosition) { + Pair result = null; + + if (text != null) { + // find start of relevant substring + int cursor = cursorPosition; + if (cursor < 1) { + return null; + } + int start = 0; + while (cursor > 0) { + cursor--; + if (Character.isWhitespace(text.charAt(cursor))) { + start = cursor + 1; + break; + } + } + if (text.length()-1 < start || text.charAt(start) != '@') { + return null; + } + + // find end of relevant substring + cursor = cursorPosition; + int textLength = text.length(); + int end = textLength; + while (cursor < textLength) { + if (Character.isWhitespace(text.charAt(cursor))) { + end = cursor; + break; + } + cursor++; + } + + start++; + result = Pair.create(text.substring(start, end), start); + + } + + return result; + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/AssigneeAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/AssigneeAdapter.java new file mode 100644 index 000000000..aa8c3e8f6 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/AssigneeAdapter.java @@ -0,0 +1,80 @@ +package it.niedermann.nextcloud.deck.ui.card.details; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.core.util.Consumer; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.List; + +import it.niedermann.nextcloud.deck.databinding.ItemAssigneeBinding; +import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.model.User; + +import static androidx.recyclerview.widget.RecyclerView.NO_ID; + +@SuppressWarnings("WeakerAccess") +public class AssigneeAdapter extends RecyclerView.Adapter<AssigneeViewHolder> { + + private final Account account; + @NonNull + private List<User> users = new ArrayList<>(); + @NonNull + private final Consumer<User> userClickedListener; + + AssigneeAdapter( + @NonNull Consumer<User> userClickedListener, + @NonNull Account account + ) { + super(); + this.userClickedListener = userClickedListener; + this.account = account; + setHasStableIds(true); + } + + @Override + public long getItemId(int position) { + Long id = users.get(position).getLocalId(); + return id == null ? NO_ID : id; + } + + @NonNull + @Override + public AssigneeViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + final Context context = parent.getContext(); + return new AssigneeViewHolder(ItemAssigneeBinding.inflate(LayoutInflater.from(context))); + } + + @Override + public void onBindViewHolder(@NonNull AssigneeViewHolder holder, int position) { + final User user = users.get(position); + holder.bind(account, user, userClickedListener); + } + + @Override + public int getItemCount() { + return users.size(); + } + + public void setUsers(@NonNull List<User> users) { + this.users.clear(); + this.users.addAll(users); + notifyDataSetChanged(); + } + + public void addUser(@NonNull User user) { + this.users.add(user); + notifyItemInserted(this.users.size()); + } + + public void removeUser(@NonNull User user) { + final int index = this.users.indexOf(user); + this.users.remove(user); + notifyItemRemoved(index); + } + +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/AssigneeDecoration.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/AssigneeDecoration.java new file mode 100644 index 000000000..096dcfa53 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/AssigneeDecoration.java @@ -0,0 +1,28 @@ +package it.niedermann.nextcloud.deck.ui.card.details; + +import android.graphics.Rect; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Px; +import androidx.recyclerview.widget.RecyclerView; + +public class AssigneeDecoration extends RecyclerView.ItemDecoration { + + private final int gutter; + + public AssigneeDecoration(@Px int gutter) { + this.gutter = gutter; + } + + @Override + public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { + final int position = parent.getChildAdapterPosition(view); + + if (position >= 0) { + // All columns get some spacing at the bottom and at the right side + outRect.right = gutter; + outRect.bottom = gutter; + } + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/AssigneeViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/AssigneeViewHolder.java new file mode 100644 index 000000000..ddb1236b6 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/AssigneeViewHolder.java @@ -0,0 +1,29 @@ +package it.niedermann.nextcloud.deck.ui.card.details; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.util.Consumer; +import androidx.recyclerview.widget.RecyclerView; + +import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.databinding.ItemAssigneeBinding; +import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.model.User; +import it.niedermann.nextcloud.deck.util.ViewUtil; + +public class AssigneeViewHolder extends RecyclerView.ViewHolder { + private ItemAssigneeBinding binding; + + @SuppressWarnings("WeakerAccess") + public AssigneeViewHolder(ItemAssigneeBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + + public void bind(@NonNull Account account, @NonNull User user, @Nullable Consumer<User> onClickListener) { + ViewUtil.addAvatar(binding.avatar, account.getUrl(), user.getUid(), R.drawable.ic_person_grey600_24dp); + if(onClickListener != null) { + itemView.setOnClickListener((v) -> onClickListener.accept(user)); + } + } +}
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardDetailsFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardDetailsFragment.java index 3182fffa2..361984fdb 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardDetailsFragment.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardDetailsFragment.java @@ -2,7 +2,6 @@ package it.niedermann.nextcloud.deck.ui.card.details; import android.content.Context; import android.content.res.ColorStateList; -import android.graphics.Color; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.text.Editable; @@ -10,17 +9,18 @@ import android.text.TextWatcher; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.ImageView; import android.widget.LinearLayout; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.Px; import androidx.appcompat.app.AppCompatActivity; +import androidx.core.content.ContextCompat; import androidx.core.graphics.ColorUtils; import androidx.core.graphics.drawable.DrawableCompat; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.GridLayoutManager; import com.google.android.material.chip.Chip; import com.google.android.material.snackbar.Snackbar; @@ -37,6 +37,8 @@ import java.util.Calendar; import java.util.Date; import java.util.Locale; +import it.niedermann.android.util.ColorUtil; +import it.niedermann.android.util.DimensionUtil; import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.FragmentCardEditTabDetailsBinding; @@ -51,21 +53,23 @@ import it.niedermann.nextcloud.deck.ui.branding.BrandedTimePickerDialog; import it.niedermann.nextcloud.deck.ui.card.EditCardViewModel; import it.niedermann.nextcloud.deck.ui.card.LabelAutoCompleteAdapter; import it.niedermann.nextcloud.deck.ui.card.UserAutoCompleteAdapter; +import it.niedermann.nextcloud.deck.ui.card.assignee.CardAssigneeDialog; +import it.niedermann.nextcloud.deck.ui.card.assignee.CardAssigneeListener; import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment; -import it.niedermann.nextcloud.deck.util.ColorUtil; import it.niedermann.nextcloud.deck.util.MarkDownUtil; -import it.niedermann.nextcloud.deck.util.ViewUtil; import static android.text.format.DateFormat.getDateFormat; +import static android.view.View.GONE; +import static android.view.View.VISIBLE; import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce; import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.applyBrandToEditText; -import static it.niedermann.nextcloud.deck.util.DimensionUtil.dpToPx; -public class CardDetailsFragment extends BrandedFragment implements OnDateSetListener, OnTimeSetListener { +public class CardDetailsFragment extends BrandedFragment implements OnDateSetListener, OnTimeSetListener, CardAssigneeListener { private FragmentCardEditTabDetailsBinding binding; private EditCardViewModel viewModel; private SyncManager syncManager; + private AssigneeAdapter adapter; private DateFormat dateFormat; private DateFormat dueTime = new SimpleDateFormat("HH:mm", Locale.ROOT); @Px @@ -105,20 +109,20 @@ public class CardDetailsFragment extends BrandedFragment implements OnDateSetLis syncManager = new SyncManager(requireContext()); - avatarSize = dpToPx(requireContext(), R.dimen.avatar_size); + avatarSize = DimensionUtil.INSTANCE.dpToPx(requireContext(), R.dimen.avatar_size); avatarLayoutParams = new LinearLayout.LayoutParams(avatarSize, avatarSize); - avatarLayoutParams.setMargins(0, 0, dpToPx(requireContext(), R.dimen.spacer_1x), 0); + avatarLayoutParams.setMargins(0, 0, DimensionUtil.INSTANCE.dpToPx(requireContext(), R.dimen.spacer_1x), 0); - setupPeople(); + setupAssignees(); setupLabels(); setupDueDate(); setupDescription(); + setupProjects(); binding.description.setText(viewModel.getFullCard().getCard().getDescription()); return binding.getRoot(); } - @Override public void onResume() { super.onResume(); @@ -213,9 +217,9 @@ public class CardDetailsFragment extends BrandedFragment implements OnDateSetLis if (this.viewModel.getFullCard().getCard().getDueDate() != null) { binding.dueDateDate.setText(dateFormat.format(this.viewModel.getFullCard().getCard().getDueDate())); binding.dueDateTime.setText(dueTime.format(this.viewModel.getFullCard().getCard().getDueDate())); - binding.clearDueDate.setVisibility(View.VISIBLE); + binding.clearDueDate.setVisibility(VISIBLE); } else { - binding.clearDueDate.setVisibility(View.GONE); + binding.clearDueDate.setVisibility(GONE); binding.dueDateDate.setText(null); binding.dueDateTime.setText(null); } @@ -242,12 +246,12 @@ public class CardDetailsFragment extends BrandedFragment implements OnDateSetLis binding.dueDateDate.setText(null); binding.dueDateTime.setText(null); viewModel.getFullCard().getCard().setDueDate(null); - binding.clearDueDate.setVisibility(View.GONE); + binding.clearDueDate.setVisibility(GONE); }); } else { binding.dueDateDate.setEnabled(false); binding.dueDateTime.setEnabled(false); - binding.clearDueDate.setVisibility(View.GONE); + binding.clearDueDate.setVisibility(GONE); } } @@ -277,14 +281,14 @@ public class CardDetailsFragment extends BrandedFragment implements OnDateSetLis ((LabelAutoCompleteAdapter) binding.labels.getAdapter()).exclude(createdLabel); viewModel.getFullCard().getLabels().add(createdLabel); binding.labelsGroup.addView(createChipFromLabel(newLabel)); - binding.labelsGroup.setVisibility(View.VISIBLE); + binding.labelsGroup.setVisibility(VISIBLE); } }); } else { ((LabelAutoCompleteAdapter) binding.labels.getAdapter()).exclude(label); viewModel.getFullCard().getLabels().add(label); binding.labelsGroup.addView(createChipFromLabel(label)); - binding.labelsGroup.setVisibility(View.VISIBLE); + binding.labelsGroup.setVisibility(VISIBLE); } binding.labels.setText(""); @@ -296,18 +300,17 @@ public class CardDetailsFragment extends BrandedFragment implements OnDateSetLis for (Label label : viewModel.getFullCard().getLabels()) { binding.labelsGroup.addView(createChipFromLabel(label)); } - binding.labelsGroup.setVisibility(View.VISIBLE); + binding.labelsGroup.setVisibility(VISIBLE); } else { binding.labelsGroup.setVisibility(View.INVISIBLE); } } - private Chip createChipFromLabel(Label label) { final Chip chip = new Chip(activity); chip.setText(label.getTitle()); if (viewModel.canEdit()) { - chip.setCloseIcon(getResources().getDrawable(R.drawable.ic_close_circle_grey600)); + chip.setCloseIcon(ContextCompat.getDrawable(requireContext(), R.drawable.ic_close_circle_grey600)); chip.setCloseIconVisible(true); chip.setOnCloseIconClickListener(v -> { binding.labelsGroup.removeView(chip); @@ -316,9 +319,9 @@ public class CardDetailsFragment extends BrandedFragment implements OnDateSetLis }); } try { - final int labelColor = Color.parseColor("#" + label.getColor()); + final int labelColor = label.getColor(); chip.setChipBackgroundColor(ColorStateList.valueOf(labelColor)); - final int color = ColorUtil.getForegroundColorForBackgroundColor(labelColor); + final int color = ColorUtil.INSTANCE.getForegroundColorForBackgroundColor(labelColor); chip.setTextColor(color); if (chip.getCloseIcon() != null) { @@ -331,7 +334,15 @@ public class CardDetailsFragment extends BrandedFragment implements OnDateSetLis return chip; } - private void setupPeople() { + private void setupAssignees() { + adapter = new AssigneeAdapter((user) -> CardAssigneeDialog.newInstance(user).show(getChildFragmentManager(), CardAssigneeDialog.class.getSimpleName()), viewModel.getAccount()); + binding.assignees.setAdapter(adapter); + binding.assignees.post(() -> { + @Px final int gutter = DimensionUtil.INSTANCE.dpToPx(requireContext(), R.dimen.spacer_1x); + final int spanCount = (int) (float) binding.assignees.getWidth() / (DimensionUtil.INSTANCE.dpToPx(requireContext(), R.dimen.avatar_size) + gutter); + binding.assignees.setLayoutManager(new GridLayoutManager(getContext(), spanCount)); + binding.assignees.addItemDecoration(new AssigneeDecoration(gutter)); + }); if (viewModel.canEdit()) { Long localCardId = viewModel.getFullCard().getCard().getLocalId(); localCardId = localCardId == null ? -1 : localCardId; @@ -340,46 +351,20 @@ public class CardDetailsFragment extends BrandedFragment implements OnDateSetLis User user = (User) adapterView.getItemAtPosition(position); viewModel.getFullCard().getAssignedUsers().add(user); ((UserAutoCompleteAdapter) binding.people.getAdapter()).exclude(user); - addAvatar(viewModel.getAccount().getUrl(), user); + adapter.addUser(user); binding.people.setText(""); }); if (this.viewModel.getFullCard().getAssignedUsers() != null) { - binding.peopleList.removeAllViews(); - for (User user : this.viewModel.getFullCard().getAssignedUsers()) { - addAvatar(viewModel.getAccount().getUrl(), user); - } + adapter.setUsers(this.viewModel.getFullCard().getAssignedUsers()); } } else { binding.people.setEnabled(false); } } - private void addAvatar(String baseUrl, User user) { - ImageView avatar = new ImageView(activity); - avatar.setLayoutParams(avatarLayoutParams); - if (viewModel.canEdit()) { - avatar.setOnClickListener(v -> { - viewModel.getFullCard().getAssignedUsers().remove(user); - binding.peopleList.removeView(avatar); - ((UserAutoCompleteAdapter) binding.people.getAdapter()).include(user); - BrandedSnackbar.make( - requireView(), getString(R.string.unassigned_user, user.getDisplayname()), - Snackbar.LENGTH_LONG) - .setAction(R.string.simple_undo, v1 -> { - viewModel.getFullCard().getAssignedUsers().add(user); - ((UserAutoCompleteAdapter) binding.people.getAdapter()).exclude(user); - addAvatar(baseUrl, user); - }).show(); - }); - } - binding.peopleList.addView(avatar); - avatar.requestLayout(); - ViewUtil.addAvatar(avatar, baseUrl, user.getUid(), avatarSize, R.drawable.ic_person_grey600_24dp); - } - @Override - public void onDateSet(com.wdullaer.materialdatetimepicker.date.DatePickerDialog view, int year, int monthOfYear, int dayOfMonth) { + public void onDateSet(DatePickerDialog view, int year, int monthOfYear, int dayOfMonth) { Calendar c = Calendar.getInstance(); int hourOfDay; int minute; @@ -397,14 +382,14 @@ public class CardDetailsFragment extends BrandedFragment implements OnDateSetLis binding.dueDateDate.setText(dateFormat.format(c.getTime())); if (this.viewModel.getFullCard().getCard().getDueDate() == null || this.viewModel.getFullCard().getCard().getDueDate().getTime() == 0) { - binding.clearDueDate.setVisibility(View.GONE); + binding.clearDueDate.setVisibility(GONE); } else { - binding.clearDueDate.setVisibility(View.VISIBLE); + binding.clearDueDate.setVisibility(VISIBLE); } } @Override - public void onTimeSet(com.wdullaer.materialdatetimepicker.time.TimePickerDialog view, int hourOfDay, int minute, int second) { + public void onTimeSet(TimePickerDialog view, int hourOfDay, int minute, int second) { if (this.viewModel.getFullCard().getCard().getDueDate() == null) { this.viewModel.getFullCard().getCard().setDueDate(new Date()); } @@ -412,9 +397,37 @@ public class CardDetailsFragment extends BrandedFragment implements OnDateSetLis this.viewModel.getFullCard().getCard().getDueDate().setMinutes(minute); binding.dueDateTime.setText(dueTime.format(this.viewModel.getFullCard().getCard().getDueDate().getTime())); if (this.viewModel.getFullCard().getCard().getDueDate() == null || this.viewModel.getFullCard().getCard().getDueDate().getTime() == 0) { - binding.clearDueDate.setVisibility(View.GONE); + binding.clearDueDate.setVisibility(GONE); } else { - binding.clearDueDate.setVisibility(View.VISIBLE); + binding.clearDueDate.setVisibility(VISIBLE); } } + + private void setupProjects() { + if (viewModel.getFullCard().getProjects().size() > 0) { + binding.projectsTitle.setVisibility(VISIBLE); + binding.projects.setNestedScrollingEnabled(false); + final CardProjectsAdapter adapter = new CardProjectsAdapter(viewModel.getFullCard().getProjects(), getChildFragmentManager()); + binding.projects.setAdapter(adapter); + binding.projects.setVisibility(VISIBLE); + } else { + binding.projectsTitle.setVisibility(GONE); + binding.projects.setVisibility(GONE); + } + } + + @Override + public void onUnassignUser(@NonNull User user) { + viewModel.getFullCard().getAssignedUsers().remove(user); + adapter.removeUser(user); + ((UserAutoCompleteAdapter) binding.people.getAdapter()).include(user); + BrandedSnackbar.make( + requireView(), getString(R.string.unassigned_user, user.getDisplayname()), + Snackbar.LENGTH_LONG) + .setAction(R.string.simple_undo, v1 -> { + viewModel.getFullCard().getAssignedUsers().add(user); + ((UserAutoCompleteAdapter) binding.people.getAdapter()).exclude(user); + adapter.addUser(user); + }).show(); + } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardDetailsListener.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardDetailsListener.java deleted file mode 100644 index 2efbab789..000000000 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardDetailsListener.java +++ /dev/null @@ -1,21 +0,0 @@ -package it.niedermann.nextcloud.deck.ui.card.details; - -import java.util.Date; - -import it.niedermann.nextcloud.deck.model.Label; -import it.niedermann.nextcloud.deck.model.User; - -public interface CardDetailsListener { - - void onDescriptionChanged(String toString); - - void onDueDateChanged(Date dueDate); - - void onUserAdded(User user); - - void onUserRemoved(User user); - - void onLabelRemoved(Label label); - - void onLabelAdded(Label createdLabel); -}
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardProjectsAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardProjectsAdapter.java new file mode 100644 index 000000000..0c2d63d74 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardProjectsAdapter.java @@ -0,0 +1,52 @@ +package it.niedermann.nextcloud.deck.ui.card.details; + +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.fragment.app.FragmentManager; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.List; + +import it.niedermann.nextcloud.deck.databinding.ItemProjectBinding; +import it.niedermann.nextcloud.deck.model.ocs.projects.full.OcsProjectWithResources; +import it.niedermann.nextcloud.deck.ui.card.projectresources.CardProjectResourcesDialog; + +public class CardProjectsAdapter extends RecyclerView.Adapter<CardProjectsViewHolder> { + + @NonNull + private final List<OcsProjectWithResources> projects; + @NonNull + private final FragmentManager fragmentManager; + + public CardProjectsAdapter(@NonNull List<OcsProjectWithResources> projects, @NonNull FragmentManager fragmentManager) { + this.projects = new ArrayList<>(projects.size()); + this.projects.addAll(projects); + this.fragmentManager = fragmentManager; + setHasStableIds(true); + } + + @Override + public long getItemId(int position) { + return projects.get(position).getLocalId(); + } + + @NonNull + @Override + public CardProjectsViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return new CardProjectsViewHolder(ItemProjectBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); + } + + @Override + public void onBindViewHolder(@NonNull CardProjectsViewHolder holder, int position) { + final OcsProjectWithResources project = projects.get(position); + holder.bind(project, (v) -> CardProjectResourcesDialog.newInstance(project.getName(), project.getResources()).show(fragmentManager, CardProjectResourcesDialog.class.getSimpleName())); + } + + @Override + public int getItemCount() { + return projects.size(); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardProjectsViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardProjectsViewHolder.java new file mode 100644 index 000000000..9c5494af7 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/CardProjectsViewHolder.java @@ -0,0 +1,32 @@ +package it.niedermann.nextcloud.deck.ui.card.details; + +import android.view.View.OnClickListener; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; + +import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.databinding.ItemProjectBinding; +import it.niedermann.nextcloud.deck.model.ocs.projects.full.OcsProjectWithResources; + +public class CardProjectsViewHolder extends RecyclerView.ViewHolder { + + private ItemProjectBinding binding; + + public CardProjectsViewHolder(@NonNull ItemProjectBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + + public void bind(@NonNull OcsProjectWithResources project, @Nullable OnClickListener onClickListener) { + binding.projectName.setText(project.getName()); + final int resourcesCount = project.getResources().size(); + binding.resourcesCount.setText(itemView.getContext().getResources().getQuantityString(R.plurals.resources_count, resourcesCount, resourcesCount)); + if (resourcesCount > 0) { + binding.getRoot().setOnClickListener(onClickListener); + } else { + binding.getRoot().setOnClickListener(null); + } + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/projectresources/CardProjectResourceAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/projectresources/CardProjectResourceAdapter.java new file mode 100644 index 000000000..b6747a11c --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/projectresources/CardProjectResourceAdapter.java @@ -0,0 +1,54 @@ +package it.niedermann.nextcloud.deck.ui.card.projectresources; + +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.lifecycle.LifecycleOwner; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.List; + +import it.niedermann.nextcloud.deck.databinding.ItemProjectResourceBinding; +import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProjectResource; + +public class CardProjectResourceAdapter extends RecyclerView.Adapter<CardProjectResourceViewHolder> { + + @NonNull + private final Account account; + @NonNull + private final List<OcsProjectResource> resources; + @NonNull + private final LifecycleOwner owner; + + public CardProjectResourceAdapter(@NonNull Account account, @NonNull List<OcsProjectResource> resources, @NonNull LifecycleOwner owner) { + this.account = account; + this.resources = new ArrayList<>(resources.size()); + this.resources.addAll(resources); + this.owner = owner; + setHasStableIds(true); + } + + @Override + public long getItemId(int position) { + return resources.get(position).getLocalId(); + } + + @NonNull + @Override + public CardProjectResourceViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return new CardProjectResourceViewHolder(ItemProjectResourceBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false)); + } + + @Override + public void onBindViewHolder(@NonNull CardProjectResourceViewHolder holder, int position) { + holder.bind(account, resources.get(position), owner); + } + + @Override + public int getItemCount() { + return this.resources.size(); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/projectresources/CardProjectResourceViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/projectresources/CardProjectResourceViewHolder.java new file mode 100644 index 000000000..d599ad5b5 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/projectresources/CardProjectResourceViewHolder.java @@ -0,0 +1,110 @@ +package it.niedermann.nextcloud.deck.ui.card.projectresources; + +import android.content.Intent; +import android.content.res.Resources; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.LifecycleOwner; +import androidx.recyclerview.widget.RecyclerView; + +import it.niedermann.nextcloud.deck.DeckLog; +import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.databinding.ItemProjectResourceBinding; +import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProjectResource; +import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; +import it.niedermann.nextcloud.deck.ui.card.EditActivity; +import it.niedermann.nextcloud.deck.util.ProjectUtil; + +import static android.view.View.GONE; +import static android.view.View.VISIBLE; +import static it.niedermann.nextcloud.deck.util.ProjectUtil.getResourceUri; + +public class CardProjectResourceViewHolder extends RecyclerView.ViewHolder { + @NonNull + private final ItemProjectResourceBinding binding; + + public CardProjectResourceViewHolder(@NonNull ItemProjectResourceBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + + public void bind(@NonNull Account account, @NonNull OcsProjectResource resource, @NonNull LifecycleOwner owner) { + final Resources resources = itemView.getResources(); + binding.name.setText(resource.getName()); + final @Nullable String link = resource.getLink(); + binding.type.setVisibility(VISIBLE); + final SyncManager syncManager = new SyncManager(itemView.getContext()); + if (resource.getType() != null) { + switch (resource.getType()) { + case "deck": { + // TODO https://github.com/stefan-niedermann/nextcloud-deck/issues/671 + linkifyViewHolder(account, link); + binding.type.setText(resources.getString(R.string.project_type_deck_board)); + binding.image.setImageResource(R.drawable.project_deck_36dp); + break; + } + case "deck-card": { + try { + long[] ids = ProjectUtil.extractBoardIdAndCardIdFromUrl(link); + if (ids.length == 2) { + syncManager.getCardByRemoteID(account.getId(), ids[1]).observe(owner, (fullCard) -> { + if (fullCard != null) { + syncManager.getBoardByRemoteId(account.getId(), ids[0]).observe(owner, (board) -> { + if (board != null) { + binding.getRoot().setOnClickListener((v) -> itemView.getContext().startActivity(EditActivity.createEditCardIntent(itemView.getContext(), account, board.getLocalId(), fullCard.getLocalId()))); + } else { + linkifyViewHolder(account, link); + } + }); + } else { + linkifyViewHolder(account, link); + } + }); + } else { + linkifyViewHolder(account, link); + } + } catch (IllegalArgumentException e) { + DeckLog.logError(e); + linkifyViewHolder(account, link); + } + binding.type.setText(resources.getString(R.string.project_type_deck_card)); + binding.image.setImageResource(R.drawable.project_deck_36dp); + break; + } + case "file": { + binding.type.setText(resources.getString(R.string.project_type_file)); + linkifyViewHolder(account, link); + binding.image.setImageResource(R.drawable.project_file_36dp); + break; + } + case "room": { + binding.type.setText(resources.getString(R.string.project_type_room)); + linkifyViewHolder(account, link); + binding.image.setImageResource(R.drawable.project_talk_36dp); + break; + } + default: { + DeckLog.info("Unknown resource type for " + resource.getName() + ": " + resource.getType()); + binding.type.setVisibility(GONE); + linkifyViewHolder(account, link); + break; + } + } + } else { + DeckLog.warn("Resource type for " + resource.getName() + " is null"); + binding.type.setVisibility(GONE); + } + } + + private void linkifyViewHolder(@NonNull Account account, @Nullable String link) { + if (link != null) { + try { + binding.getRoot().setOnClickListener((v) -> itemView.getContext().startActivity(new Intent(Intent.ACTION_VIEW).setData(getResourceUri(account, link)))); + } catch (IllegalArgumentException e) { + DeckLog.logError(e); + } + } + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/projectresources/CardProjectResourcesDialog.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/projectresources/CardProjectResourcesDialog.java new file mode 100644 index 000000000..9dc056634 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/projectresources/CardProjectResourcesDialog.java @@ -0,0 +1,83 @@ +package it.niedermann.nextcloud.deck.ui.card.projectresources; + +import android.app.Dialog; +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.DialogFragment; +import androidx.lifecycle.ViewModelProvider; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.databinding.DialogProjectResourcesBinding; +import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProjectResource; +import it.niedermann.nextcloud.deck.ui.branding.BrandedAlertDialogBuilder; +import it.niedermann.nextcloud.deck.ui.branding.BrandedDialogFragment; +import it.niedermann.nextcloud.deck.ui.card.EditCardViewModel; + +public class CardProjectResourcesDialog extends BrandedDialogFragment { + + private static final String KEY_RESOURCES = "resources"; + private static final String KEY_PROJECT_NAME = "projectName"; + private DialogProjectResourcesBinding binding; + private EditCardViewModel viewModel; + + private String projectName; + @NonNull + private List<OcsProjectResource> resources = new ArrayList<>(); + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + final Bundle args = requireArguments(); + if (!args.containsKey(KEY_RESOURCES)) { + throw new IllegalArgumentException("Provide at least " + KEY_RESOURCES); + } + //noinspection unchecked + this.resources.addAll((ArrayList<OcsProjectResource>) Objects.requireNonNull(args.getSerializable(KEY_RESOURCES))); + this.projectName = args.getString(KEY_PROJECT_NAME); + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + binding = DialogProjectResourcesBinding.inflate(LayoutInflater.from(requireContext())); + viewModel = new ViewModelProvider(requireActivity()).get(EditCardViewModel.class); + + AlertDialog.Builder dialogBuilder = new BrandedAlertDialogBuilder(requireContext()); + + return dialogBuilder + .setTitle(projectName) + .setView(binding.getRoot()) + .setNeutralButton(R.string.simple_close, null) + .create(); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + final CardProjectResourceAdapter adapter = new CardProjectResourceAdapter(viewModel.getAccount(), resources, requireActivity()); + binding.getRoot().setAdapter(adapter); + super.onActivityCreated(savedInstanceState); + } + + @Override + public void applyBrand(int mainColor) { + + } + + public static DialogFragment newInstance(@Nullable String projectName, @NonNull List<OcsProjectResource> resources) { + final DialogFragment fragment = new CardProjectResourcesDialog(); + final Bundle args = new Bundle(); + args.putString(KEY_PROJECT_NAME, projectName); + args.putSerializable(KEY_RESOURCES, new ArrayList<>(resources)); + fragment.setArguments(args); + return fragment; + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionActivity.java index 9eef878c3..ac6335b90 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionActivity.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionActivity.java @@ -8,13 +8,12 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; -import it.niedermann.nextcloud.deck.DeckLog; +import it.niedermann.android.util.ClipboardUtil; +import it.niedermann.nextcloud.deck.BuildConfig; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.ActivityExceptionBinding; import it.niedermann.nextcloud.deck.ui.exception.tips.TipsAdapter; -import it.niedermann.nextcloud.deck.util.ExceptionUtil; - -import static it.niedermann.nextcloud.deck.util.ClipboardUtil.copyToClipboard; +import it.niedermann.nextcloud.exception.ExceptionUtil; public class ExceptionActivity extends AppCompatActivity { @@ -22,9 +21,12 @@ public class ExceptionActivity extends AppCompatActivity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + final ActivityExceptionBinding binding = ActivityExceptionBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); - super.onCreate(savedInstanceState); + setSupportActionBar(binding.toolbar); Throwable throwable = ((Throwable) getIntent().getSerializableExtra(KEY_THROWABLE)); @@ -32,23 +34,18 @@ public class ExceptionActivity extends AppCompatActivity { throwable = new Exception("Could not get exception"); } - DeckLog.logError(throwable); + final TipsAdapter adapter = new TipsAdapter(this::startActivity); + final String debugInfo = "Full Crash:\n\n" + ExceptionUtil.INSTANCE.getDebugInfos(this, throwable, BuildConfig.FLAVOR); - setSupportActionBar(binding.toolbar); + binding.tips.setAdapter(adapter); + binding.tips.setNestedScrollingEnabled(false); binding.toolbar.setTitle(R.string.error); binding.message.setText(throwable.getMessage()); - - final String debugInfo = ExceptionUtil.getDebugInfos(this, throwable, null); - binding.stacktrace.setText(debugInfo); + binding.copy.setOnClickListener((v) -> ClipboardUtil.INSTANCE.copyToClipboard(this, getString(R.string.simple_exception), "```\n" + debugInfo + "\n```")); + binding.close.setOnClickListener((v) -> finish()); - final TipsAdapter adapter = new TipsAdapter(this::startActivity); - binding.tips.setAdapter(adapter); - binding.tips.setNestedScrollingEnabled(false); adapter.setThrowable(this, null, throwable); - - binding.copy.setOnClickListener((v) -> copyToClipboard(this, getString(R.string.simple_exception), "```\n" + debugInfo + "\n```")); - binding.close.setOnClickListener((v) -> finish()); } @NonNull diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionDialogFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionDialogFragment.java index 6c0d0ba79..7a84ce0b6 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionDialogFragment.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionDialogFragment.java @@ -11,14 +11,14 @@ import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatDialogFragment; import androidx.fragment.app.DialogFragment; +import it.niedermann.android.util.ClipboardUtil; +import it.niedermann.nextcloud.deck.BuildConfig; import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.DialogExceptionBinding; import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.ui.exception.tips.TipsAdapter; -import it.niedermann.nextcloud.deck.util.ExceptionUtil; - -import static it.niedermann.nextcloud.deck.util.ClipboardUtil.copyToClipboard; +import it.niedermann.nextcloud.exception.ExceptionUtil; public class ExceptionDialogFragment extends AppCompatDialogFragment { @@ -52,7 +52,7 @@ public class ExceptionDialogFragment extends AppCompatDialogFragment { final TipsAdapter adapter = new TipsAdapter((actionIntent) -> requireActivity().startActivity(actionIntent)); - final String debugInfos = ExceptionUtil.getDebugInfos(requireContext(), throwable, account); + final String debugInfos = ExceptionUtil.INSTANCE.getDebugInfos(requireContext(), throwable, BuildConfig.FLAVOR, account == null ? null : account.getServerDeckVersion()); binding.tips.setAdapter(adapter); binding.stacktrace.setText(debugInfos); @@ -65,7 +65,7 @@ public class ExceptionDialogFragment extends AppCompatDialogFragment { .setView(binding.getRoot()) .setTitle(R.string.error_dialog_title) .setPositiveButton(android.R.string.copy, (a, b) -> { - copyToClipboard(requireContext(), getString(R.string.simple_exception), "```\n" + debugInfos + "\n```"); + ClipboardUtil.INSTANCE.copyToClipboard(requireContext(), getString(R.string.simple_exception), "```\n" + debugInfos + "\n```"); a.dismiss(); }) .setNegativeButton(R.string.simple_close, null) diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionHandler.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionHandler.java index 53c99d52c..c62b23e51 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionHandler.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/ExceptionHandler.java @@ -4,19 +4,22 @@ import android.app.Activity; import androidx.annotation.NonNull; +import it.niedermann.nextcloud.deck.DeckLog; + public class ExceptionHandler implements Thread.UncaughtExceptionHandler { - private Activity context; + @NonNull + private final Activity activity; - public ExceptionHandler(Activity context) { - super(); - this.context = context; + public ExceptionHandler(@NonNull Activity activity) { + this.activity = activity; } @Override - public void uncaughtException(@NonNull Thread t, Throwable e) { - context.getApplicationContext().startActivity(ExceptionActivity.createIntent(context, e)); - context.finish(); + public void uncaughtException(@NonNull Thread t, @NonNull Throwable e) { + DeckLog.logError(e); + activity.getApplicationContext().startActivity(ExceptionActivity.createIntent(activity, e)); + activity.finish(); Runtime.getRuntime().exit(0); } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/tips/TipsAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/tips/TipsAdapter.java index a059b2956..6bfd82b13 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/tips/TipsAdapter.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/exception/tips/TipsAdapter.java @@ -39,6 +39,10 @@ import static it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment. public class TipsAdapter extends RecyclerView.Adapter<TipsViewHolder> { + private static final Intent INTENT_APP_INFO = new Intent(ACTION_APPLICATION_DETAILS_SETTINGS) + .setData(Uri.parse("package:" + BuildConfig.APPLICATION_ID)) + .putExtra(INTENT_EXTRA_BUTTON_TEXT, R.string.error_action_open_deck_info); + @NonNull private Consumer<Intent> actionButtonClickedListener; @NonNull @@ -68,11 +72,8 @@ public class TipsAdapter extends RecyclerView.Adapter<TipsViewHolder> { public void setThrowable(@NonNull Context context, @Nullable Account account, @NonNull Throwable throwable) { if (throwable instanceof TokenMismatchException) { add(R.string.error_dialog_tip_token_mismatch_retry); - add(R.string.error_dialog_tip_token_mismatch_clear_storage); - Intent intent = new Intent(ACTION_APPLICATION_DETAILS_SETTINGS) - .setData(Uri.parse("package:" + BuildConfig.APPLICATION_ID)) - .putExtra(INTENT_EXTRA_BUTTON_TEXT, R.string.error_action_open_deck_info); - add(R.string.error_dialog_tip_clear_storage, intent); + add(R.string.error_dialog_tip_clear_storage_might_help); + add(R.string.error_dialog_tip_clear_storage, INTENT_APP_INFO); } else if (throwable instanceof NextcloudFilesAppNotSupportedException) { add(R.string.error_dialog_tip_files_outdated); } else if (throwable instanceof NextcloudApiNotRespondingException) { @@ -122,6 +123,10 @@ public class TipsAdapter extends RecyclerView.Adapter<TipsViewHolder> { } else { add(R.string.error_dialog_version_not_parsable); } + add(R.string.error_dialog_account_might_not_be_authorized); + break; + case UNKNOWN_ACCOUNT_USER_ID: + add(R.string.error_dialog_user_not_found_in_database); break; case CAPABILITIES_NOT_PARSABLE: default: @@ -133,15 +138,15 @@ public class TipsAdapter extends RecyclerView.Adapter<TipsViewHolder> { add(R.string.error_dialog_capabilities_not_parsable); } } + // Files app might no longer be authenticated: https://github.com/stefan-niedermann/nextcloud-deck/issues/621#issuecomment-665533567 + add(R.string.error_dialog_tip_clear_storage_might_help); + add(R.string.error_dialog_tip_clear_storage, INTENT_APP_INFO); } else if (throwable instanceof RuntimeException) { if (throwable.getMessage() != null && throwable.getMessage().contains("database")) { Intent reportIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(context.getString(R.string.url_report_bug))) .putExtra(INTENT_EXTRA_BUTTON_TEXT, R.string.error_action_report_issue); add(R.string.error_dialog_tip_database_upgrade_failed, reportIntent); - Intent clearIntent = new Intent(ACTION_APPLICATION_DETAILS_SETTINGS) - .setData(Uri.parse("package:" + BuildConfig.APPLICATION_ID)) - .putExtra(INTENT_EXTRA_BUTTON_TEXT, R.string.error_action_open_deck_info); - add(R.string.error_dialog_tip_clear_storage, clearIntent); + add(R.string.error_dialog_tip_clear_storage, INTENT_APP_INFO); } } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterDialogFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterDialogFragment.java index b6a0c17a4..6aa03b811 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterDialogFragment.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterDialogFragment.java @@ -20,6 +20,7 @@ import androidx.viewpager2.widget.ViewPager2; import com.google.android.material.tabs.TabLayoutMediator; +import it.niedermann.android.util.ColorUtil; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.DialogFilterBinding; import it.niedermann.nextcloud.deck.model.enums.EDueType; @@ -28,7 +29,6 @@ import it.niedermann.nextcloud.deck.ui.branding.BrandedAlertDialogBuilder; import it.niedermann.nextcloud.deck.ui.branding.BrandedDialogFragment; import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.getSecondaryForegroundColorDependingOnTheme; -import static it.niedermann.nextcloud.deck.util.ColorUtil.getContrastRatio; public class FilterDialogFragment extends BrandedDialogFragment { @@ -47,8 +47,9 @@ public class FilterDialogFragment extends BrandedDialogFragment { public Dialog onCreateDialog(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - indicator = getResources().getDrawable(R.drawable.circle_grey600_8dp); - indicator.setColorFilter(getResources().getColor(R.color.primary), PorterDuff.Mode.SRC_ATOP); + indicator = ContextCompat.getDrawable(requireContext(), R.drawable.circle_grey600_8dp); + assert indicator != null; + indicator.setColorFilter(getResources().getColor(R.color.defaultBrand), PorterDuff.Mode.SRC_ATOP); filterViewModel = new ViewModelProvider(requireActivity()).get(FilterViewModel.class); @@ -63,10 +64,10 @@ public class FilterDialogFragment extends BrandedDialogFragment { filterInformationDraft.observe(this, (draft) -> { switch (position) { case 0: - tab.setIcon(draft.getLabels().size() > 0 ? indicator : null); + tab.setIcon(draft.getLabels().size() > 0 || draft.isNoAssignedLabel() ? indicator : null); break; case 1: - tab.setIcon(draft.getUsers().size() > 0 ? indicator : null); + tab.setIcon(draft.getUsers().size() > 0 || draft.isNoAssignedUser() ? indicator : null); break; case 2: tab.setIcon(draft.getDueType() != EDueType.NO_FILTER ? indicator : null); @@ -106,7 +107,7 @@ public class FilterDialogFragment extends BrandedDialogFragment { @Override public void applyBrand(int mainColor) { @ColorInt final int finalMainColor = getSecondaryForegroundColorDependingOnTheme(binding.tabLayout.getContext(), mainColor); - final boolean contrastRatioIsSufficient = getContrastRatio(mainColor, ContextCompat.getColor(binding.tabLayout.getContext(), R.color.primary)) > 1.7d; + final boolean contrastRatioIsSufficient = ColorUtil.INSTANCE.getContrastRatio(mainColor, ContextCompat.getColor(binding.tabLayout.getContext(), R.color.primary)) > 1.7d; binding.tabLayout.setSelectedTabIndicatorColor(contrastRatioIsSufficient ? mainColor : finalMainColor); indicator.setColorFilter(contrastRatioIsSufficient ? mainColor : finalMainColor, PorterDuff.Mode.SRC_ATOP); } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterLabelsAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterLabelsAdapter.java index 096f0db9c..39fb791be 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterLabelsAdapter.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterLabelsAdapter.java @@ -1,20 +1,21 @@ package it.niedermann.nextcloud.deck.ui.filter; import android.content.res.ColorStateList; -import android.graphics.Color; import android.view.LayoutInflater; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; import androidx.recyclerview.widget.RecyclerView; import java.util.ArrayList; import java.util.List; +import it.niedermann.android.util.ColorUtil; +import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.ItemFilterLabelBinding; import it.niedermann.nextcloud.deck.model.Label; -import it.niedermann.nextcloud.deck.util.ColorUtil; @SuppressWarnings("WeakerAccess") public class FilterLabelsAdapter extends RecyclerView.Adapter<FilterLabelsAdapter.LabelViewHolder> { @@ -23,11 +24,17 @@ public class FilterLabelsAdapter extends RecyclerView.Adapter<FilterLabelsAdapte @NonNull private final List<Label> selectedLabels = new ArrayList<>(); @Nullable + private static final Label NOT_ASSIGNED = null; + @Nullable private final SelectionListener<Label> selectionListener; - public FilterLabelsAdapter(@NonNull List<Label> labels, @NonNull List<Label> selectedLabels, @Nullable SelectionListener<Label> selectionListener) { + public FilterLabelsAdapter(@NonNull List<Label> labels, @NonNull List<Label> selectedLabels, boolean noAssignedLabel, @Nullable SelectionListener<Label> selectionListener) { super(); + this.labels.add(NOT_ASSIGNED); this.labels.addAll(labels); + if (noAssignedLabel) { + this.selectedLabels.add(NOT_ASSIGNED); + } this.selectedLabels.addAll(selectedLabels); this.selectionListener = selectionListener; setHasStableIds(true); @@ -36,7 +43,8 @@ public class FilterLabelsAdapter extends RecyclerView.Adapter<FilterLabelsAdapte @Override public long getItemId(int position) { - return labels.get(position).getLocalId(); + @Nullable final Label label = labels.get(position); + return label == null ? -1L : label.getLocalId(); } @NonNull @@ -47,7 +55,11 @@ public class FilterLabelsAdapter extends RecyclerView.Adapter<FilterLabelsAdapte @Override public void onBindViewHolder(@NonNull LabelViewHolder viewHolder, int position) { - viewHolder.bind(labels.get(position)); + if (position == 0) { + viewHolder.bindNotAssigned(); + } else { + viewHolder.bind(labels.get(position)); + } } @Override @@ -55,26 +67,36 @@ public class FilterLabelsAdapter extends RecyclerView.Adapter<FilterLabelsAdapte return labels.size(); } - public List<Label> getSelected() { - return selectedLabels; - } - class LabelViewHolder extends RecyclerView.ViewHolder { private ItemFilterLabelBinding binding; LabelViewHolder(@NonNull ItemFilterLabelBinding binding) { super(binding.getRoot()); this.binding = binding; + this.binding.label.setClickable(false); } void bind(final Label label) { binding.label.setText(label.getTitle()); - final int labelColor = Color.parseColor("#" + label.getColor()); + final int labelColor = label.getColor(); binding.label.setChipBackgroundColor(ColorStateList.valueOf(labelColor)); - final int color = ColorUtil.getForegroundColorForBackgroundColor(labelColor); + final int color = ColorUtil.INSTANCE.getForegroundColorForBackgroundColor(labelColor); binding.label.setTextColor(color); itemView.setSelected(selectedLabels.contains(label)); + bindClickListener(label); + } + + public void bindNotAssigned() { + binding.label.setText(itemView.getContext().getString(R.string.no_assigned_label)); + binding.label.setTextColor(ColorStateList.valueOf(ContextCompat.getColor(itemView.getContext(), R.color.accent))); + binding.label.setChipIcon(ContextCompat.getDrawable(itemView.getContext(), R.drawable.ic_baseline_block_24)); + binding.label.setChipBackgroundColor(ColorStateList.valueOf(ContextCompat.getColor(itemView.getContext(), R.color.primary))); + binding.label.setRippleColor(null); + itemView.setSelected(selectedLabels.contains(NOT_ASSIGNED)); + bindClickListener(NOT_ASSIGNED); + } + private void bindClickListener(@Nullable Label label) { itemView.setOnClickListener(view -> { if (selectedLabels.contains(label)) { selectedLabels.remove(label); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterLabelsFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterLabelsFragment.java index 357f93cf9..fadf33b88 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterLabelsFragment.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterLabelsFragment.java @@ -33,19 +33,31 @@ public class FilterLabelsFragment extends Fragment implements SelectionListener< observeOnce(new SyncManager(requireContext()).findProposalsForLabelsToAssign(mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId()), requireActivity(), (labels) -> { binding.labels.setNestedScrollingEnabled(false); - binding.labels.setAdapter(new FilterLabelsAdapter(labels, requireNonNull(filterViewModel.getFilterInformationDraft().getValue()).getLabels(), this)); + binding.labels.setAdapter(new FilterLabelsAdapter( + labels, + requireNonNull(filterViewModel.getFilterInformationDraft().getValue()).getLabels(), + requireNonNull(filterViewModel.getFilterInformationDraft().getValue()).isNoAssignedLabel(), + this)); }); return binding.getRoot(); } @Override - public void onItemSelected(Label item) { - filterViewModel.addFilterInformationDraftLabel(item); + public void onItemSelected(@Nullable Label item) { + if (item == null) { + filterViewModel.setNotAssignedLabel(true); + } else { + filterViewModel.addFilterInformationDraftLabel(item); + } } @Override - public void onItemDeselected(Label item) { - filterViewModel.removeFilterInformationLabel(item); + public void onItemDeselected(@Nullable Label item) { + if (item == null) { + filterViewModel.setNotAssignedLabel(false); + } else { + filterViewModel.removeFilterInformationLabel(item); + } } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserAdapter.java index b4ae8f679..82230060c 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserAdapter.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserAdapter.java @@ -8,6 +8,8 @@ import androidx.annotation.Nullable; import androidx.annotation.Px; import androidx.recyclerview.widget.RecyclerView; +import com.bumptech.glide.Glide; + import java.util.ArrayList; import java.util.List; @@ -23,6 +25,8 @@ public class FilterUserAdapter extends RecyclerView.Adapter<FilterUserAdapter.Us final int avatarSize; @NonNull private final Account account; + @Nullable + private static final User NOT_ASSIGNED = null; @NonNull private final List<User> users = new ArrayList<>(); @NonNull @@ -30,11 +34,15 @@ public class FilterUserAdapter extends RecyclerView.Adapter<FilterUserAdapter.Us @Nullable private final SelectionListener<User> selectionListener; - public FilterUserAdapter(@Px int avatarSize, @NonNull Account account, @NonNull List<User> users, @NonNull List<User> selectedUsers, @Nullable SelectionListener selectionListener) { + public FilterUserAdapter(@Px int avatarSize, @NonNull Account account, @NonNull List<User> users, @NonNull List<User> selectedUsers, boolean noAssignedUser, @Nullable SelectionListener<User> selectionListener) { super(); this.avatarSize = avatarSize; this.account = account; + this.users.add(NOT_ASSIGNED); this.users.addAll(users); + if (noAssignedUser) { + this.selectedUsers.add(NOT_ASSIGNED); + } this.selectedUsers.addAll(selectedUsers); this.selectionListener = selectionListener; setHasStableIds(true); @@ -43,7 +51,8 @@ public class FilterUserAdapter extends RecyclerView.Adapter<FilterUserAdapter.Us @Override public long getItemId(int position) { - return users.get(position).getLocalId(); + @Nullable final User user = users.get(position); + return user == null ? -1L : user.getLocalId(); } @NonNull @@ -54,7 +63,11 @@ public class FilterUserAdapter extends RecyclerView.Adapter<FilterUserAdapter.Us @Override public void onBindViewHolder(@NonNull UserViewHolder viewHolder, int position) { - viewHolder.bind(users.get(position)); + if (position == 0) { + viewHolder.bindNotAssigned(); + } else { + viewHolder.bind(users.get(position)); + } } @Override @@ -62,10 +75,6 @@ public class FilterUserAdapter extends RecyclerView.Adapter<FilterUserAdapter.Us return users.size(); } - public List<User> getSelected() { - return selectedUsers; - } - class UserViewHolder extends RecyclerView.ViewHolder { private ItemFilterUserBinding binding; @@ -74,22 +83,34 @@ public class FilterUserAdapter extends RecyclerView.Adapter<FilterUserAdapter.Us this.binding = binding; } - void bind(final User user) { + void bind(@NonNull final User user) { binding.displayName.setText(user.getDisplayname()); ViewUtil.addAvatar(binding.avatar, account.getUrl(), user.getUid(), avatarSize, R.drawable.ic_person_grey600_24dp); itemView.setSelected(selectedUsers.contains(user)); + bindClickListener(user); + } + + public void bindNotAssigned() { + binding.displayName.setText(itemView.getContext().getString(R.string.simple_unassigned)); + Glide.with(itemView.getContext()) + .load(R.drawable.ic_baseline_block_24) + .into(binding.avatar); + itemView.setSelected(selectedUsers.contains(NOT_ASSIGNED)); + bindClickListener(NOT_ASSIGNED); + } + private void bindClickListener(@Nullable User user) { itemView.setOnClickListener(view -> { if (selectedUsers.contains(user)) { selectedUsers.remove(user); itemView.setSelected(false); - if(selectionListener != null) { + if (selectionListener != null) { selectionListener.onItemDeselected(user); } } else { selectedUsers.add(user); itemView.setSelected(true); - if(selectionListener != null) { + if (selectionListener != null) { selectionListener.onItemSelected(user); } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserFragment.java index 64bc1db1f..ffd558b65 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserFragment.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserFragment.java @@ -10,6 +10,7 @@ import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; +import it.niedermann.android.util.DimensionUtil; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.DialogFilterAssigneesBinding; import it.niedermann.nextcloud.deck.model.User; @@ -17,7 +18,6 @@ import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; import it.niedermann.nextcloud.deck.ui.MainViewModel; import static it.niedermann.nextcloud.deck.persistence.sync.adapters.db.util.LiveDataHelper.observeOnce; -import static it.niedermann.nextcloud.deck.util.DimensionUtil.dpToPx; import static java.util.Objects.requireNonNull; public class FilterUserFragment extends Fragment implements SelectionListener<User> { @@ -35,19 +35,33 @@ public class FilterUserFragment extends Fragment implements SelectionListener<Us observeOnce(new SyncManager(requireContext()).findProposalsForUsersToAssign(mainViewModel.getCurrentAccount().getId(), mainViewModel.getCurrentBoardLocalId()), requireActivity(), (users) -> { binding.users.setNestedScrollingEnabled(false); - binding.users.setAdapter(new FilterUserAdapter(dpToPx(requireContext(), R.dimen.avatar_size), mainViewModel.getCurrentAccount(), users, requireNonNull(filterViewModel.getFilterInformationDraft().getValue()).getUsers(), this)); + binding.users.setAdapter(new FilterUserAdapter( + DimensionUtil.INSTANCE.dpToPx(requireContext(), R.dimen.avatar_size), + mainViewModel.getCurrentAccount(), + users, + requireNonNull(filterViewModel.getFilterInformationDraft().getValue()).getUsers(), + requireNonNull(filterViewModel.getFilterInformationDraft().getValue()).isNoAssignedUser(), + this)); }); return binding.getRoot(); } @Override - public void onItemSelected(User item) { - filterViewModel.addFilterInformationUser(item); + public void onItemSelected(@Nullable User item) { + if (item == null) { + filterViewModel.setNotAssignedUser(true); + } else { + filterViewModel.addFilterInformationUser(item); + } } @Override - public void onItemDeselected(User item) { - filterViewModel.removeFilterInformationUser(item); + public void onItemDeselected(@Nullable User item) { + if (item == null) { + filterViewModel.setNotAssignedUser(false); + } else { + filterViewModel.removeFilterInformationUser(item); + } } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterViewModel.java index cf8dc1754..50f287d62 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterViewModel.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterViewModel.java @@ -66,6 +66,18 @@ public class FilterViewModel extends ViewModel { this.filterInformationDraft.postValue(newDraft); } + public void setNotAssignedUser(boolean notAssignedUser) { + FilterInformation newDraft = new FilterInformation(filterInformationDraft.getValue()); + newDraft.setNoAssignedUser(notAssignedUser); + this.filterInformationDraft.postValue(newDraft); + } + + public void setNotAssignedLabel(boolean notAssignedLabel) { + FilterInformation newDraft = new FilterInformation(filterInformationDraft.getValue()); + newDraft.setNoAssignedLabel(notAssignedLabel); + this.filterInformationDraft.postValue(newDraft); + } + public void removeFilterInformationLabel(@NonNull Label label) { FilterInformation newDraft = new FilterInformation(filterInformationDraft.getValue()); newDraft.removeLabel(label); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/SelectionListener.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/SelectionListener.java index d2635a860..3fad71773 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/SelectionListener.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/SelectionListener.java @@ -1,9 +1,11 @@ package it.niedermann.nextcloud.deck.ui.filter; +import androidx.annotation.Nullable; + public interface SelectionListener<T> { - void onItemSelected(T item); + void onItemSelected(@Nullable T item); - default void onItemDeselected(T item) { + default void onItemDeselected(@Nullable T item) { // Deselecting is optional } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountViewHolder.java index 4b43cbed6..0892eb437 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountViewHolder.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountViewHolder.java @@ -11,14 +11,14 @@ import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; import com.bumptech.glide.request.RequestOptions; -import it.niedermann.android.glidesso.SingleSignOnUrl; +import it.niedermann.android.util.DimensionUtil; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.ItemAccountChooseBinding; import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.sso.glide.SingleSignOnUrl; import static android.view.View.GONE; import static android.view.View.VISIBLE; -import static it.niedermann.nextcloud.deck.util.DimensionUtil.dpToPx; public class ManageAccountViewHolder extends RecyclerView.ViewHolder { @@ -33,7 +33,7 @@ public class ManageAccountViewHolder extends RecyclerView.ViewHolder { binding.accountName.setText(account.getUserName()); binding.accountHost.setText(Uri.parse(account.getUrl()).getHost()); Glide.with(itemView.getContext()) - .load(new SingleSignOnUrl(account.getName(), account.getAvatarUrl(dpToPx(binding.accountItemAvatar.getContext(), R.dimen.avatar_size)))) + .load(new SingleSignOnUrl(account.getName(), account.getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(binding.accountItemAvatar.getContext(), R.dimen.avatar_size)))) .placeholder(R.drawable.ic_baseline_account_circle_24) .error(R.drawable.ic_baseline_account_circle_24) .apply(RequestOptions.circleCropTransform()) diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/movecard/MoveCardDialogFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/movecard/MoveCardDialogFragment.java new file mode 100644 index 000000000..b14b60a7a --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/movecard/MoveCardDialogFragment.java @@ -0,0 +1,126 @@ +package it.niedermann.nextcloud.deck.ui.movecard; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.DialogFragment; + +import it.niedermann.nextcloud.deck.DeckLog; +import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.databinding.DialogMoveCardBinding; +import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.model.Board; +import it.niedermann.nextcloud.deck.model.Stack; +import it.niedermann.nextcloud.deck.ui.branding.BrandedDialogFragment; +import it.niedermann.nextcloud.deck.ui.branding.BrandingUtil; +import it.niedermann.nextcloud.deck.ui.pickstack.PickStackFragment; +import it.niedermann.nextcloud.deck.ui.pickstack.PickStackListener; + +import static android.view.View.GONE; +import static android.view.View.VISIBLE; + +public class MoveCardDialogFragment extends BrandedDialogFragment implements PickStackListener { + + private static final String KEY_ORIGIN_ACCOUNT_ID = "account_id"; + private static final String KEY_ORIGIN_BOARD_LOCAL_ID = "board_local_id"; + private static final String KEY_ORIGIN_CARD_TITLE = "card_title"; + private static final String KEY_ORIGIN_CARD_LOCAL_ID = "card_local_id"; + private Long originAccountId; + private Long originBoardLocalId; + private String originCardTitle; + private Long originCardLocalId; + + private DialogMoveCardBinding binding; + private MoveCardListener moveCardListener; + + private Account selectedAccount; + private Board selectedBoard; + private Stack selectedStack; + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + if (getParentFragment() instanceof MoveCardListener) { + this.moveCardListener = (MoveCardListener) getParentFragment(); + } else if (context instanceof MoveCardListener) { + this.moveCardListener = (MoveCardListener) context; + } else { + throw new IllegalArgumentException("Caller must implement " + MoveCardListener.class.getSimpleName()); + } + + final Bundle args = requireArguments(); + originAccountId = args.getLong(KEY_ORIGIN_ACCOUNT_ID, -1L); + if (originAccountId < 0) { + throw new IllegalArgumentException("Missing " + KEY_ORIGIN_ACCOUNT_ID); + } + originCardLocalId = args.getLong(KEY_ORIGIN_CARD_LOCAL_ID, -1L); + if (originCardLocalId < 0) { + throw new IllegalArgumentException("Missing " + KEY_ORIGIN_CARD_LOCAL_ID); + } + originBoardLocalId = args.getLong(KEY_ORIGIN_BOARD_LOCAL_ID, -1L); + if (originBoardLocalId < 0) { + throw new IllegalArgumentException("Missing " + KEY_ORIGIN_BOARD_LOCAL_ID); + } + originCardTitle = args.getString(KEY_ORIGIN_CARD_TITLE); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + binding = DialogMoveCardBinding.inflate(inflater); + binding.title.setText(getString(R.string.action_card_move_title, originCardTitle)); + binding.submit.setOnClickListener((v) -> { + DeckLog.verbose("[Move card] Attempt to move to " + Stack.class.getSimpleName() + " #" + selectedStack.getLocalId()); + this.moveCardListener.move(originAccountId, originCardLocalId, selectedAccount.getId(), selectedBoard.getLocalId(), selectedStack.getLocalId()); + dismiss(); + }); + binding.cancel.setOnClickListener((v) -> dismiss()); + return binding.getRoot(); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + getChildFragmentManager() + .beginTransaction() + .add(R.id.fragment_container, PickStackFragment.newInstance(false)) + .commit(); + } + + @Override + public void onStackPicked(@NonNull Account account, @Nullable Board board, @Nullable Stack stack) { + this.selectedAccount = account; + this.selectedBoard = board; + this.selectedStack = stack; + if (board == null || stack == null) { + binding.submit.setEnabled(false); + binding.moveWarning.setVisibility(GONE); + } else { + binding.submit.setEnabled(true); + binding.moveWarning.setVisibility(board.getLocalId().equals(originBoardLocalId) ? GONE : VISIBLE); + } + } + + @Override + public void applyBrand(int mainColor) { + final ColorStateList mainColorStateList = ColorStateList.valueOf(BrandingUtil.getSecondaryForegroundColorDependingOnTheme(requireContext(), mainColor)); + binding.cancel.setTextColor(mainColorStateList); + binding.submit.setTextColor(mainColorStateList); + } + + public static DialogFragment newInstance(long originAccountId, long originBoardLocalId, String originCardTitle, Long originCardLocalId) { + final DialogFragment dialogFragment = new MoveCardDialogFragment(); + final Bundle args = new Bundle(); + args.putLong(KEY_ORIGIN_ACCOUNT_ID, originAccountId); + args.putLong(KEY_ORIGIN_BOARD_LOCAL_ID, originBoardLocalId); + args.putString(KEY_ORIGIN_CARD_TITLE, originCardTitle); + args.putLong(KEY_ORIGIN_CARD_LOCAL_ID, originCardLocalId); + dialogFragment.setArguments(args); + return dialogFragment; + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/movecard/MoveCardListener.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/movecard/MoveCardListener.java new file mode 100644 index 000000000..f6f7a7a1f --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/movecard/MoveCardListener.java @@ -0,0 +1,5 @@ +package it.niedermann.nextcloud.deck.ui.movecard; + +public interface MoveCardListener { + void move(long originAccountId, long originCardLocalId, long targetAccountId, long targetBoardLocalId, long targetStackLocalId); +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackFragment.java new file mode 100644 index 000000000..8e0d7179a --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackFragment.java @@ -0,0 +1,206 @@ +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.databinding.FragmentPickStackBinding; +import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.model.Board; +import it.niedermann.nextcloud.deck.model.Stack; +import it.niedermann.nextcloud.deck.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 static final String KEY_SHOW_BOARDS_WITHOUT_EDIT_PERMISSION = "show_boards_without_edit_permission"; + + private SyncManager syncManager; + + private PickStackListener pickStackListener; + + private boolean showBoardsWithoutEditPermission = false; + private long lastAccountId; + private long lastBoardId; + private long lastStackId; + + private ArrayAdapter<Account> accountAdapter; + private ArrayAdapter<Board> boardAdapter; + private ArrayAdapter<Stack> stackAdapter; + + @Nullable + private LiveData<List<Board>> boardsLiveData; + @NonNull + private Observer<List<Board>> boardsObserver = (boards) -> { + boardAdapter.clear(); + boardAdapter.addAll(boards); + binding.boardSelect.setEnabled(true); + + if (boards.size() > 0) { + binding.boardSelect.setEnabled(true); + + Board boardToSelect = null; + for (Board board : boards) { + if (board.getLocalId() == lastBoardId) { + boardToSelect = board; + break; + } + } + if (boardToSelect == null) { + boardToSelect = boards.get(0); + } + binding.boardSelect.setSelection(boardAdapter.getPosition(boardToSelect)); + } else { + binding.boardSelect.setEnabled(false); + pickStackListener.onStackPicked((Account) binding.accountSelect.getSelectedItem(), null, null); + } + }; + + @Nullable + private LiveData<List<Stack>> stacksLiveData; + @NonNull + private Observer<List<Stack>> stacksObserver = (stacks) -> { + stackAdapter.clear(); + stackAdapter.addAll(stacks); + + if (stacks.size() > 0) { + binding.stackSelect.setEnabled(true); + + Stack stackToSelect = null; + for (Stack stack : stacks) { + if (stack.getLocalId() == lastStackId) { + stackToSelect = stack; + break; + } + } + if (stackToSelect == null) { + stackToSelect = stacks.get(0); + } + binding.stackSelect.setSelection(stackAdapter.getPosition(stackToSelect)); + } else { + binding.stackSelect.setEnabled(false); + pickStackListener.onStackPicked((Account) binding.accountSelect.getSelectedItem(), (Board) binding.boardSelect.getSelectedItem(), null); + } + }; + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + if (getParentFragment() instanceof PickStackListener) { + this.pickStackListener = (PickStackListener) getParentFragment(); + } else if (context instanceof PickStackListener) { + this.pickStackListener = (PickStackListener) context; + } else { + throw new IllegalArgumentException("Caller must implement " + PickStackListener.class.getSimpleName()); + } + final Bundle args = getArguments(); + if (args != null) { + this.showBoardsWithoutEditPermission = args.getBoolean(KEY_SHOW_BOARDS_WITHOUT_EDIT_PERMISSION, false); + } + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + binding = FragmentPickStackBinding.inflate(getLayoutInflater()); + + 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, showBoardsWithoutEditPermission + ? syncManager.getBoards(parent.getSelectedItemId()) + : 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(), (Stack) parent.getSelectedItem()); + }); + + return binding.getRoot(); + } + + /** + * Updates the source of the given liveData and de- and reregisters the given observer. + */ + private <T> void updateLiveDataSource(@Nullable LiveData<T> liveData, Observer<T> observer, LiveData<T> newSource) { + if (liveData != null) { + liveData.removeObserver(observer); + } + liveData = newSource; + liveData.observe(getViewLifecycleOwner(), observer); + } + + public static PickStackFragment newInstance(boolean showBoardsWithoutEditPermission) { + final PickStackFragment fragment = new PickStackFragment(); + final Bundle args = new Bundle(); + args.putBoolean(KEY_SHOW_BOARDS_WITHOUT_EDIT_PERMISSION, showBoardsWithoutEditPermission); + fragment.setArguments(args); + return fragment; + } +}
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackListener.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackListener.java new file mode 100644 index 000000000..ce227746a --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/pickstack/PickStackListener.java @@ -0,0 +1,12 @@ +package it.niedermann.nextcloud.deck.ui.pickstack; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.model.Board; +import it.niedermann.nextcloud.deck.model.Stack; + +public interface PickStackListener { + void onStackPicked(@NonNull Account account, @Nullable Board board, @Nullable Stack stack); +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/AccountAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/AccountAdapter.java index af368b92a..f537c9fb4 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/AccountAdapter.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/AccountAdapter.java @@ -6,12 +6,17 @@ import android.view.ViewGroup; import androidx.annotation.NonNull; +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.RequestOptions; + +import java.net.URL; + +import it.niedermann.android.util.DimensionUtil; import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.ItemPrepareCreateAccountBinding; import it.niedermann.nextcloud.deck.model.Account; -import it.niedermann.nextcloud.deck.util.DimensionUtil; -import it.niedermann.nextcloud.deck.util.ViewUtil; +import it.niedermann.nextcloud.sso.glide.SingleSignOnUrl; public class AccountAdapter extends AbstractAdapter<Account> { @@ -38,8 +43,18 @@ public class AccountAdapter extends AbstractAdapter<Account> { final Account item = getItem(position); if (item != null) { binding.username.setText(item.getUserName()); - binding.instance.setText(item.getUrl()); - ViewUtil.addAvatar(binding.avatar, item.getUrl(), item.getUserName(), DimensionUtil.dpToPx(binding.avatar.getContext(), R.dimen.icon_size_details), R.drawable.ic_person_grey600_24dp); + try { + binding.instance.setText(new URL(item.getUrl()).getHost()); + } catch (Throwable t) { + binding.instance.setText(item.getUrl()); + } + + Glide.with(getContext()) + .load(new SingleSignOnUrl(item.getName(), item.getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(binding.avatar.getContext(), R.dimen.icon_size_details)))) + .placeholder(R.drawable.ic_baseline_account_circle_24) + .error(R.drawable.ic_baseline_account_circle_24) + .apply(RequestOptions.circleCropTransform()) + .into(binding.avatar); } else { DeckLog.logError(new IllegalArgumentException("No item for position " + position)); } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/BoardAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/BoardAdapter.java index 5cda9e877..558a22db4 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/BoardAdapter.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/BoardAdapter.java @@ -37,7 +37,7 @@ public class BoardAdapter extends AbstractAdapter<Board> { final Board item = getItem(position); if (item != null) { binding.boardTitle.setText(item.getTitle()); - binding.avatar.setImageDrawable(ViewUtil.getTintedImageView(binding.avatar.getContext(), R.drawable.circle_grey600_36dp, "#" + item.getColor())); + binding.avatar.setImageDrawable(ViewUtil.getTintedImageView(binding.avatar.getContext(), R.drawable.circle_grey600_36dp, String.format("#%06X", (0xFFFFFF & item.getColor())))); } else { DeckLog.logError(new IllegalArgumentException("No item for position " + position)); } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/PrepareCreateActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/PrepareCreateActivity.java index ab0cc4816..18317e078 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/PrepareCreateActivity.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/PrepareCreateActivity.java @@ -2,214 +2,52 @@ package it.niedermann.nextcloud.deck.ui.preparecreate; import android.content.ClipData; import android.content.Intent; -import android.content.res.ColorStateList; -import android.graphics.Color; import android.os.Bundle; import android.text.TextUtils; -import android.widget.ArrayAdapter; -import androidx.annotation.ColorInt; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.appcompat.app.AppCompatActivity; -import androidx.core.content.ContextCompat; -import androidx.core.graphics.drawable.DrawableCompat; -import androidx.lifecycle.LiveData; -import androidx.lifecycle.Observer; +import androidx.appcompat.app.ActionBar; -import java.util.List; - -import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.R; -import it.niedermann.nextcloud.deck.databinding.ActivityPrepareCreateBinding; import it.niedermann.nextcloud.deck.model.Account; -import it.niedermann.nextcloud.deck.model.Board; -import it.niedermann.nextcloud.deck.model.full.FullStack; -import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; -import it.niedermann.nextcloud.deck.ui.ImportAccountActivity; -import it.niedermann.nextcloud.deck.ui.branding.Branded; +import it.niedermann.nextcloud.deck.ui.PickStackActivity; import it.niedermann.nextcloud.deck.ui.card.EditActivity; -import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment; -import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler; -import it.niedermann.nextcloud.deck.util.ColorUtil; -import static android.graphics.Color.parseColor; -import static androidx.lifecycle.Transformations.switchMap; -import static it.niedermann.nextcloud.deck.DeckApplication.isDarkTheme; -import static it.niedermann.nextcloud.deck.DeckApplication.readCurrentAccountId; -import static it.niedermann.nextcloud.deck.DeckApplication.readCurrentBoardId; -import static it.niedermann.nextcloud.deck.DeckApplication.readCurrentStackId; import static it.niedermann.nextcloud.deck.DeckApplication.saveCurrentAccountId; import static it.niedermann.nextcloud.deck.DeckApplication.saveCurrentBoardId; import static it.niedermann.nextcloud.deck.DeckApplication.saveCurrentStackId; -import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.getSecondaryForegroundColorDependingOnTheme; -import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.isBrandingEnabled; -import static it.niedermann.nextcloud.deck.util.ColorUtil.contrastRatioIsSufficientBigAreas; - -public class PrepareCreateActivity extends AppCompatActivity implements Branded { - - private ActivityPrepareCreateBinding binding; - - private SyncManager syncManager; - - private boolean brandingEnabled; - - private long lastAccountId; - private long lastBoardId; - private long lastStackId; - private ArrayAdapter<Account> accountAdapter; - private ArrayAdapter<Board> boardAdapter; - private ArrayAdapter<FullStack> stackAdapter; - - @Nullable - private LiveData<List<Board>> boardsLiveData; - @NonNull - private Observer<List<Board>> boardsObserver = (boards) -> { - boardAdapter.clear(); - boardAdapter.addAll(boards); - binding.boardSelect.setEnabled(true); - - if (boards.size() > 0) { - binding.boardSelect.setEnabled(true); - - for (Board board : boards) { - if (board.getLocalId() == lastBoardId) { - binding.boardSelect.setSelection(boardAdapter.getPosition(board)); - applyBrand(Color.parseColor('#' + board.getColor())); - break; - } - } - } else { - applyBrand(ContextCompat.getColor(this, R.color.defaultBrand)); - binding.boardSelect.setEnabled(false); - binding.submit.setEnabled(false); - } - }; - - @Nullable - private LiveData<List<FullStack>> stacksLiveData; - @NonNull - private Observer<List<FullStack>> stacksObserver = (fullStacks) -> { - stackAdapter.clear(); - stackAdapter.addAll(fullStacks); - - if (fullStacks.size() > 0) { - binding.stackSelect.setEnabled(true); - binding.submit.setEnabled(true); - - for (FullStack fullStack : fullStacks) { - if (fullStack.getLocalId() == lastStackId) { - binding.stackSelect.setSelection(stackAdapter.getPosition(fullStack)); - break; - } - } - } else { - binding.stackSelect.setEnabled(false); - binding.submit.setEnabled(false); - } - }; +public class PrepareCreateActivity extends PickStackActivity { @Override - protected void onCreate(Bundle savedInstanceState) { + public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - - Thread.setDefaultUncaughtExceptionHandler(new ExceptionHandler(this)); - - brandingEnabled = isBrandingEnabled(this); - - binding = ActivityPrepareCreateBinding.inflate(getLayoutInflater()); - setContentView(binding.getRoot()); - setSupportActionBar(binding.toolbar); - - accountAdapter = new AccountAdapter(this); - binding.accountSelect.setAdapter(accountAdapter); - binding.accountSelect.setEnabled(false); - boardAdapter = new BoardAdapter(this); - binding.boardSelect.setAdapter(boardAdapter); - binding.stackSelect.setEnabled(false); - stackAdapter = new StackAdapter(this); - binding.stackSelect.setAdapter(stackAdapter); - binding.stackSelect.setEnabled(false); - - syncManager = new SyncManager(this); - - switchMap(syncManager.hasAccounts(), hasAccounts -> { - if (hasAccounts) { - return syncManager.readAccounts(); - } else { - startActivityForResult(new Intent(this, ImportAccountActivity.class), ImportAccountActivity.REQUEST_CODE_IMPORT_ACCOUNT); - return null; - } - }).observe(this, (List<Account> accounts) -> { - if (accounts == null || accounts.size() == 0) { - throw new IllegalStateException("hasAccounts() returns true, but readAccounts() returns null or has no entry"); - } - - lastAccountId = readCurrentAccountId(this); - lastBoardId = readCurrentBoardId(this, lastAccountId); - lastStackId = readCurrentStackId(this, lastAccountId, lastBoardId); - - accountAdapter.clear(); - accountAdapter.addAll(accounts); - binding.accountSelect.setEnabled(true); - - for (Account account : accounts) { - if (account.getId() == lastAccountId) { - binding.accountSelect.setSelection(accountAdapter.getPosition(account)); - break; - } - } - }); - - binding.accountSelect.setOnItemSelectedListener((SelectedListener) (parent, view, position, id) -> { - updateLiveDataSource(boardsLiveData, boardsObserver, syncManager.getBoardsWithEditPermission(parent.getSelectedItemId())); - }); - - binding.boardSelect.setOnItemSelectedListener((SelectedListener) (parent, view, position, id) -> { - applyBrand(Color.parseColor('#' + ((Board) binding.boardSelect.getSelectedItem()).getColor())); - updateLiveDataSource(stacksLiveData, stacksObserver, syncManager.getStacksForBoard(binding.accountSelect.getSelectedItemId(), parent.getSelectedItemId())); - }); - - binding.cancel.setOnClickListener((v) -> finish()); - binding.submit.setOnClickListener((v) -> onSubmit()); + final ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(R.string.add_card); + } } - /** - * Updates the source of the given liveData and de- and reregisters the given observer. - */ - private <T> void updateLiveDataSource(@Nullable LiveData<T> liveData, Observer<T> observer, LiveData<T> newSource) { - if (liveData != null) { - liveData.removeObserver(observer); + @Override + protected void onSubmit(Account account, long boardId, long stackId) { + final String receivedClipData = getReceivedClipData(getIntent()); + if (receivedClipData == null) { + startActivity(EditActivity.createNewCardIntent(this, account, boardId, stackId)); + } else { + startActivity(EditActivity.createNewCardIntent(this, account, boardId, stackId, receivedClipData)); } - liveData = newSource; - liveData.observe(PrepareCreateActivity.this, observer); - } - /** - * Starts EditActivity and passes parameters. - */ - private void onSubmit() { - final Account account = accountAdapter.getItem(binding.accountSelect.getSelectedItemPosition()); - if (account != null) { - final long boardId = binding.boardSelect.getSelectedItemId(); - final long stackId = binding.stackSelect.getSelectedItemId(); - final String receivedClipData = getReceivedClipData(getIntent()); - if (receivedClipData == null) { - startActivity(EditActivity.createNewCardIntent(this, account, boardId, stackId)); - } else { - startActivity(EditActivity.createNewCardIntent(this, account, boardId, stackId, receivedClipData)); - } + saveCurrentAccountId(this, account.getId()); + saveCurrentBoardId(this, account.getId(), boardId); + saveCurrentStackId(this, account.getId(), boardId, stackId); + applyBrand(account.getColor()); - saveCurrentAccountId(this, account.getId()); - saveCurrentBoardId(this, account.getId(), boardId); - saveCurrentStackId(this, account.getId(), boardId, stackId); - applyBrand(parseColor(account.getColor())); + finish(); + } - finish(); - } else { - ExceptionDialogFragment.newInstance(new IllegalStateException("Selected account at position " + binding.accountSelect.getSelectedItemPosition() + " is null."), null).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); - } + @Override + protected boolean showBoardsWithoutEditPermission() { + return false; } @Nullable @@ -232,20 +70,4 @@ public class PrepareCreateActivity extends AppCompatActivity implements Branded final CharSequence text = item.getText(); return TextUtils.isEmpty(text) ? null : text.toString(); } - - @Override - public void applyBrand(int mainColor) { - try { - if (brandingEnabled) { - @ColorInt final int finalMainColor = contrastRatioIsSufficientBigAreas(mainColor, ContextCompat.getColor(this, R.color.primary)) - ? mainColor - : isDarkTheme(this) ? Color.WHITE : Color.BLACK; - DrawableCompat.setTintList(binding.submit.getBackground(), ColorStateList.valueOf(finalMainColor)); - binding.submit.setTextColor(ColorUtil.getForegroundColorForBackgroundColor(finalMainColor)); - binding.cancel.setTextColor(getSecondaryForegroundColorDependingOnTheme(this, mainColor)); - } - } catch (Throwable t) { - DeckLog.logError(t); - } - } }
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/StackAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/StackAdapter.java index 5d1ee22ce..e3e0ad5b5 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/StackAdapter.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/preparecreate/StackAdapter.java @@ -9,9 +9,9 @@ import androidx.annotation.NonNull; import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.ItemPrepareCreateStackBinding; -import it.niedermann.nextcloud.deck.model.full.FullStack; +import it.niedermann.nextcloud.deck.model.Stack; -public class StackAdapter extends AbstractAdapter<FullStack> { +public class StackAdapter extends AbstractAdapter<Stack> { @SuppressWarnings("WeakerAccess") public StackAdapter(@NonNull Context context) { @@ -19,7 +19,7 @@ public class StackAdapter extends AbstractAdapter<FullStack> { } @Override - protected long getItemId(@NonNull FullStack item) { + protected long getItemId(@NonNull Stack item) { return item.getLocalId(); } @@ -33,9 +33,9 @@ public class StackAdapter extends AbstractAdapter<FullStack> { binding = ItemPrepareCreateStackBinding.bind(convertView); } - final FullStack item = getItem(position); + final Stack item = getItem(position); if (item != null) { - binding.stackTitle.setText(item.getStack().getTitle()); + binding.stackTitle.setText(item.getTitle()); } else { DeckLog.logError(new IllegalArgumentException("No item for position " + position)); } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/settings/SettingsFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/settings/SettingsFragment.java index ea8d90f22..04429f225 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/settings/SettingsFragment.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/settings/SettingsFragment.java @@ -23,6 +23,7 @@ public class SettingsFragment extends PreferenceFragmentCompat implements Brande private BrandedSwitchPreference wifiOnlyPref; private BrandedSwitchPreference themePref; private BrandedSwitchPreference brandingPref; + private BrandedSwitchPreference compactPref; @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { @@ -67,6 +68,8 @@ public class SettingsFragment extends PreferenceFragmentCompat implements Brande DeckLog.error("Could not find preference with key: \"" + getString(R.string.pref_key_dark_theme) + "\""); } + compactPref = findPreference(getString(R.string.pref_key_compact)); + final ListPreference backgroundSyncPref = findPreference(getString(R.string.pref_key_background_sync)); if (backgroundSyncPref != null) { backgroundSyncPref.setOnPreferenceChangeListener((Preference preference, Object newValue) -> { @@ -92,5 +95,6 @@ public class SettingsFragment extends PreferenceFragmentCompat implements Brande wifiOnlyPref.applyBrand(mainColor); themePref.applyBrand(mainColor); brandingPref.applyBrand(mainColor); + compactPref.applyBrand(mainColor); } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/sharetarget/ShareProgressDialogFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/sharetarget/ShareProgressDialogFragment.java index 271b60489..3028ea952 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/sharetarget/ShareProgressDialogFragment.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/sharetarget/ShareProgressDialogFragment.java @@ -14,15 +14,16 @@ import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.lifecycle.ViewModelProvider; +import it.niedermann.nextcloud.deck.BuildConfig; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.DialogShareProgressBinding; import it.niedermann.nextcloud.deck.exceptions.UploadAttachmentFailedException; import it.niedermann.nextcloud.deck.ui.branding.BrandedDialogFragment; import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment; +import it.niedermann.nextcloud.exception.ExceptionUtil; import static android.graphics.PorterDuff.Mode; import static it.niedermann.nextcloud.deck.ui.branding.BrandingUtil.getSecondaryForegroundColorDependingOnTheme; -import static it.niedermann.nextcloud.deck.util.ExceptionUtil.getDebugInfos; public class ShareProgressDialogFragment extends BrandedDialogFragment { @@ -70,7 +71,7 @@ public class ShareProgressDialogFragment extends BrandedDialogFragment { binding.errorReportButton.setOnClickListener((v) -> { final StringBuilder debugInfos = new StringBuilder(exceptionsCount + " attachments failed to upload:"); for (Throwable t : exceptions) { - debugInfos.append(getDebugInfos(requireContext(), t, null)); + debugInfos.append(ExceptionUtil.INSTANCE.getDebugInfos(requireContext(), t, BuildConfig.FLAVOR)); } ExceptionDialogFragment.newInstance(new UploadAttachmentFailedException(debugInfos.toString()), null) .show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/StackAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/StackAdapter.java index e2cc83372..4fe61794a 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/StackAdapter.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/StackAdapter.java @@ -8,11 +8,11 @@ import androidx.viewpager2.adapter.FragmentStateAdapter; import java.util.ArrayList; import java.util.List; -import it.niedermann.nextcloud.deck.model.full.FullStack; +import it.niedermann.nextcloud.deck.model.Stack; public class StackAdapter extends FragmentStateAdapter { @NonNull - private List<FullStack> stackList = new ArrayList<>(); + private List<Stack> stackList = new ArrayList<>(); public StackAdapter(@NonNull FragmentActivity fragmentActivity) { super(fragmentActivity); @@ -23,7 +23,7 @@ public class StackAdapter extends FragmentStateAdapter { return stackList.size(); } - public FullStack getItem(int position) { + public Stack getItem(int position) { return stackList.get(position); } @@ -34,7 +34,7 @@ public class StackAdapter extends FragmentStateAdapter { @Override public boolean containsItem(long itemId) { - for (FullStack stack : stackList) { + for (Stack stack : stackList) { if (stack.getLocalId() == itemId) { return true; } @@ -48,9 +48,9 @@ public class StackAdapter extends FragmentStateAdapter { return StackFragment.newInstance(stackList.get(position).getLocalId()); } - public void setStacks(@NonNull List<FullStack> fullStacks) { + public void setStacks(@NonNull List<Stack> stacks) { this.stackList.clear(); - this.stackList.addAll(fullStacks); + this.stackList.addAll(stacks); notifyDataSetChanged(); } }
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/StackFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/stack/StackFragment.java index 726a74184..7b0d2f20f 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() && !SyncManager.ignoreExceptionOnVoidError(liveData.getError())) { + ExceptionDialogFragment.newInstance(liveData.getError(), null).show(getChildFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + } else { + DeckLog.log("Moved " + Card.class.getSimpleName() + " \"" + originCardLocalId + "\" to " + Stack.class.getSimpleName() + " \"" + targetStackLocalId + "\""); + } + }); + } + }
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/ColorChooser.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/ColorChooser.java index 30dc0ada4..c82f47c9c 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/ColorChooser.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/ColorChooser.java @@ -14,23 +14,17 @@ import androidx.annotation.Nullable; import com.google.android.flexbox.FlexboxLayout; import com.skydoves.colorpickerview.listeners.ColorEnvelopeListener; +import it.niedermann.android.util.DimensionUtil; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.WidgetColorChooserBinding; import it.niedermann.nextcloud.deck.util.ViewUtil; -import static it.niedermann.nextcloud.deck.util.DimensionUtil.dpToPx; - public class ColorChooser extends LinearLayout { - private WidgetColorChooserBinding binding; - - private final FlexboxLayout.LayoutParams params = new FlexboxLayout.LayoutParams( - ViewGroup.LayoutParams.WRAP_CONTENT, - ViewGroup.LayoutParams.WRAP_CONTENT - ); + private final WidgetColorChooserBinding binding; - private Context context; - private String[] colors; + private final Context context; + private final String[] colors; private String selectedColor; private String previouslySelectedColor; @@ -41,7 +35,11 @@ public class ColorChooser extends LinearLayout { super(context, attrs); this.context = context; - params.setMargins(0, dpToPx(context, R.dimen.spacer_1x), 0, 0); + final FlexboxLayout.LayoutParams params = new FlexboxLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ); + params.setMargins(0, DimensionUtil.INSTANCE.dpToPx(context, R.dimen.spacer_1x), 0, 0); params.setFlexBasisPercent(.15f); TypedArray a = context.obtainStyledAttributes(attrs, diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/OverlappingAvatars.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/OverlappingAvatars.java index 501d33106..0facc385c 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/OverlappingAvatars.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/OverlappingAvatars.java @@ -10,6 +10,7 @@ import android.widget.RelativeLayout; import androidx.annotation.NonNull; import androidx.annotation.Px; +import androidx.core.content.ContextCompat; import androidx.core.graphics.drawable.DrawableCompat; import com.bumptech.glide.Glide; @@ -17,12 +18,11 @@ import com.bumptech.glide.request.RequestOptions; import java.util.List; +import it.niedermann.android.util.DimensionUtil; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.model.User; -import static it.niedermann.nextcloud.deck.util.DimensionUtil.dpToPx; - public class OverlappingAvatars extends RelativeLayout { final int maxAvatarCount; @Px @@ -44,11 +44,12 @@ public class OverlappingAvatars extends RelativeLayout { public OverlappingAvatars(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); maxAvatarCount = context.getResources().getInteger(R.integer.max_avatar_count); - avatarBorderSize = dpToPx(context, R.dimen.avatar_size_small_overlapping_border); - avatarSize = dpToPx(context, R.dimen.avatar_size_small) + avatarBorderSize * 2; - overlapPx = dpToPx(context, R.dimen.avatar_size_small_overlapping); - borderDrawable = getResources().getDrawable(R.drawable.avatar_border); - DrawableCompat.setTint(borderDrawable, getResources().getColor(R.color.bg_card)); + avatarBorderSize = DimensionUtil.INSTANCE.dpToPx(context, R.dimen.avatar_size_small_overlapping_border); + avatarSize = DimensionUtil.INSTANCE.dpToPx(context, R.dimen.avatar_size_small) + avatarBorderSize * 2; + overlapPx = DimensionUtil.INSTANCE.dpToPx(context, R.dimen.avatar_size_small_overlapping); + borderDrawable = ContextCompat.getDrawable(context, R.drawable.avatar_border); + assert borderDrawable != null; + DrawableCompat.setTint(borderDrawable, ContextCompat.getColor(context, R.color.bg_card)); } public void setAvatars(@NonNull Account account, @NonNull List<User> assignedUsers) { @@ -70,6 +71,7 @@ public class OverlappingAvatars extends RelativeLayout { avatar.requestLayout(); Glide.with(context) .load(account.getUrl() + "/index.php/avatar/" + Uri.encode(assignedUsers.get(avatarCount).getUid()) + "/" + avatarSize) + .placeholder(R.drawable.ic_person_grey600_24dp) .error(R.drawable.ic_person_grey600_24dp) .apply(RequestOptions.circleCropTransform()) .into(avatar); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labelchip/CompactLabelChip.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labelchip/CompactLabelChip.java new file mode 100644 index 000000000..a2a50430c --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labelchip/CompactLabelChip.java @@ -0,0 +1,21 @@ +package it.niedermann.nextcloud.deck.ui.view.labelchip; + +import android.annotation.SuppressLint; +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Px; + +import it.niedermann.android.util.DimensionUtil; +import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.model.Label; + +@SuppressLint("ViewConstructor") +public class CompactLabelChip extends LabelChip { + + public CompactLabelChip(@NonNull Context context, @NonNull Label label, @Px int gutter) { + super(context, label, gutter); + params.setFlexBasisPercent(1 / 6.5f); + setHeight(DimensionUtil.INSTANCE.dpToPx(context, R.dimen.compact_label_height)); + } +}
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labelchip/DefaultLabelChip.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labelchip/DefaultLabelChip.java new file mode 100644 index 000000000..80e44d7e0 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labelchip/DefaultLabelChip.java @@ -0,0 +1,21 @@ +package it.niedermann.nextcloud.deck.ui.view.labelchip; + +import android.annotation.SuppressLint; +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Px; + +import it.niedermann.nextcloud.deck.model.Label; + +import static android.text.TextUtils.TruncateAt.MIDDLE; + +@SuppressLint("ViewConstructor") +public class DefaultLabelChip extends LabelChip { + + public DefaultLabelChip(@NonNull Context context, @NonNull Label label, @Px int gutter) { + super(context, label, gutter); + setText(label.getTitle()); + setEllipsize(MIDDLE); + } +}
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/LabelChip.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labelchip/LabelChip.java index db4e123d3..83853f0b2 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/LabelChip.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labelchip/LabelChip.java @@ -1,9 +1,8 @@ -package it.niedermann.nextcloud.deck.ui.view; +package it.niedermann.nextcloud.deck.ui.view.labelchip; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.ColorStateList; -import android.graphics.Color; import android.view.ViewGroup; import androidx.annotation.NonNull; @@ -12,26 +11,24 @@ import androidx.annotation.Px; import com.google.android.flexbox.FlexboxLayout; import com.google.android.material.chip.Chip; +import it.niedermann.android.util.ColorUtil; import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.model.Label; -import it.niedermann.nextcloud.deck.util.ColorUtil; - -import static android.text.TextUtils.TruncateAt.MIDDLE; @SuppressLint("ViewConstructor") public class LabelChip extends Chip { private final Label label; + protected final FlexboxLayout.LayoutParams params = new FlexboxLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ); + public LabelChip(@NonNull Context context, @NonNull Label label, @Px int gutter) { super(context); this.label = label; - FlexboxLayout.LayoutParams params = new FlexboxLayout.LayoutParams( - ViewGroup.LayoutParams.WRAP_CONTENT, - ViewGroup.LayoutParams.WRAP_CONTENT - ); - params.setMargins(0, 0, gutter, 0); setLayoutParams(params); setEnsureMinTouchTargetSize(false); @@ -42,15 +39,13 @@ public class LabelChip extends Chip { setTextStartPadding(gutter); setTextEndPadding(gutter); setChipEndPadding(gutter); - - setText(label.getTitle()); - setEllipsize(MIDDLE); + setClickable(false); try { - int labelColor = Color.parseColor("#" + label.getColor()); + int labelColor = label.getColor(); ColorStateList c = ColorStateList.valueOf(labelColor); setChipBackgroundColor(c); - setTextColor(ColorUtil.getForegroundColorForBackgroundColor(labelColor)); + setTextColor(ColorUtil.INSTANCE.getForegroundColorForBackgroundColor(labelColor)); } catch (IllegalArgumentException e) { DeckLog.logError(e); } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labellayout/CompactLabelLayout.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labellayout/CompactLabelLayout.java new file mode 100644 index 000000000..1c5e35d97 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labellayout/CompactLabelLayout.java @@ -0,0 +1,22 @@ +package it.niedermann.nextcloud.deck.ui.view.labellayout; + +import android.content.Context; +import android.util.AttributeSet; + +import androidx.annotation.NonNull; + +import it.niedermann.nextcloud.deck.model.Label; +import it.niedermann.nextcloud.deck.ui.view.labelchip.CompactLabelChip; +import it.niedermann.nextcloud.deck.ui.view.labelchip.LabelChip; + +public class CompactLabelLayout extends LabelLayout { + + public CompactLabelLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected LabelChip createLabelChip(@NonNull Label label) { + return new CompactLabelChip(getContext(), label, gutter); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labellayout/DefaultLabelLayout.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labellayout/DefaultLabelLayout.java new file mode 100644 index 000000000..f2d6d0752 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labellayout/DefaultLabelLayout.java @@ -0,0 +1,21 @@ +package it.niedermann.nextcloud.deck.ui.view.labellayout; + +import android.content.Context; +import android.util.AttributeSet; + +import androidx.annotation.NonNull; + +import it.niedermann.nextcloud.deck.model.Label; +import it.niedermann.nextcloud.deck.ui.view.labelchip.DefaultLabelChip; + +public class DefaultLabelLayout extends LabelLayout { + + public DefaultLabelLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected DefaultLabelChip createLabelChip(@NonNull Label label) { + return new DefaultLabelChip(getContext(), label, gutter); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/LabelLayout.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labellayout/LabelLayout.java index 814c63ce1..3db539e53 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/LabelLayout.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/labellayout/LabelLayout.java @@ -1,4 +1,4 @@ -package it.niedermann.nextcloud.deck.ui.view; +package it.niedermann.nextcloud.deck.ui.view.labellayout; import android.content.Context; import android.util.AttributeSet; @@ -11,21 +11,22 @@ import com.google.android.flexbox.FlexboxLayout; import java.util.LinkedList; import java.util.List; +import it.niedermann.android.util.DimensionUtil; import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.model.Label; +import it.niedermann.nextcloud.deck.ui.view.labelchip.LabelChip; -import static it.niedermann.nextcloud.deck.util.DimensionUtil.dpToPx; - -public class LabelLayout extends FlexboxLayout { +public abstract class LabelLayout extends FlexboxLayout { @Px - private int gutter; - private List<LabelChip> chipList = new LinkedList<>(); + final protected int gutter; + @NonNull + final private List<LabelChip> chipList = new LinkedList<>(); public LabelLayout(Context context, AttributeSet attrs) { super(context, attrs); - this.gutter = dpToPx(context, R.dimen.spacer_1hx); + this.gutter = DimensionUtil.INSTANCE.dpToPx(context, R.dimen.spacer_1hx); } /** @@ -82,9 +83,11 @@ public class LabelLayout extends FlexboxLayout { continue labelList; } } - LabelChip chip = new LabelChip(getContext(), label, gutter); + final LabelChip chip = createLabelChip(label); addView(chip); chipList.add(chip); } } + + protected abstract LabelChip createLabelChip(@NonNull Label label); } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/singlecard/SingleCardWidget.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/singlecard/SingleCardWidget.java index 783f98e00..d8e47134c 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/singlecard/SingleCardWidget.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/singlecard/SingleCardWidget.java @@ -141,13 +141,10 @@ public class SingleCardWidget extends AppWidgetProvider { super.onDeleted(context, appWidgetIds); } - /** * Updates UI data of all {@link SingleCardWidget} instances */ public static void notifyDatasetChanged(Context context) { - Intent intent = new Intent(context, SingleCardWidget.class); - intent.setAction("android.appwidget.action.APPWIDGET_UPDATE"); - context.sendBroadcast(intent); + context.sendBroadcast(new Intent(context, SingleCardWidget.class).setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE)); } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidget.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidget.java new file mode 100644 index 000000000..cb179953c --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidget.java @@ -0,0 +1,121 @@ +package it.niedermann.nextcloud.deck.ui.widget.stack; + +import android.app.PendingIntent; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.widget.RemoteViews; + +import java.util.NoSuchElementException; + +import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.model.appwidgets.StackWidgetModel; +import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; +import it.niedermann.nextcloud.deck.ui.MainActivity; +import it.niedermann.nextcloud.deck.ui.card.EditActivity; + +import static android.appwidget.AppWidgetManager.ACTION_APPWIDGET_UPDATE; + +public class StackWidget extends AppWidgetProvider { + public static final String ACCOUNT_ID_KEY = "stack_widget_account_id"; + public static final String ACCOUNT_KEY = "stack_widget_account"; + public static final String STACK_ID_KEY = "stack_widget_stack_id"; + public static final String BUNDLE_KEY = "stack_widget_bundle"; + private static final int PENDING_INTENT_OPEN_APP_RQ = 0; + private static final int PENDING_INTENT_EDIT_CARD_RQ = 1; + + static void updateAppWidget(Context context, AppWidgetManager awm, int[] appWidgetIds, Account account) { + final SyncManager syncManager = new SyncManager(context); + + for (int appWidgetId : appWidgetIds) { + new Thread(() -> { + try { + final StackWidgetModel model = syncManager.getStackWidgetModelDirectly(appWidgetId); + RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_stack); + Intent serviceIntent = new Intent(context, StackWidgetService.class); + + serviceIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); + serviceIntent.putExtra(ACCOUNT_ID_KEY + appWidgetId, model.getAccountId()); + serviceIntent.putExtra(STACK_ID_KEY + appWidgetId, model.getStackId()); + if (account != null) { + Bundle extras = new Bundle(); + extras.putSerializable(StackWidget.ACCOUNT_KEY + appWidgetId, account); + serviceIntent.putExtra(BUNDLE_KEY + appWidgetId, extras); + } + serviceIntent.setData(Uri.parse(serviceIntent.toUri(Intent.URI_INTENT_SCHEME))); + + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.setComponent(new ComponentName(context.getPackageName(), MainActivity.class.getName())); + PendingIntent pendingIntent = PendingIntent.getActivity(context, PENDING_INTENT_OPEN_APP_RQ, + intent, PendingIntent.FLAG_UPDATE_CURRENT); + views.setOnClickPendingIntent(R.id.widget_stack_header_rl, pendingIntent); + + PendingIntent templatePI = PendingIntent.getActivity(context, PENDING_INTENT_EDIT_CARD_RQ, + new Intent(context, EditActivity.class), PendingIntent.FLAG_UPDATE_CURRENT); + + views.setPendingIntentTemplate(R.id.stack_widget_lv, templatePI); + views.setRemoteAdapter(R.id.stack_widget_lv, serviceIntent); + views.setEmptyView(R.id.stack_widget_lv, R.id.widget_stack_placeholder_iv); + awm.notifyAppWidgetViewDataChanged(appWidgetId, R.id.stack_widget_lv); + awm.updateAppWidget(appWidgetId, views); + } catch (NoSuchElementException e) { + // onUpdate has been triggered before the user finished configuring the widget + } + }).start(); + } + } + + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + super.onUpdate(context, appWidgetManager, appWidgetIds); + updateAppWidget(context, appWidgetManager, appWidgetIds, null); + } + + @Override + public void onReceive(Context context, Intent intent) { + final Account account; + + super.onReceive(context, intent); + + AppWidgetManager awm = AppWidgetManager.getInstance(context); + + if (intent.getAction() != null) { + if (intent.getAction().equals(ACTION_APPWIDGET_UPDATE)) { + if (intent.hasExtra(BUNDLE_KEY)) { + Bundle extras = intent.getBundleExtra(StackWidget.BUNDLE_KEY); + account = (Account) extras.getSerializable(ACCOUNT_KEY); + + if (intent.hasExtra(AppWidgetManager.EXTRA_APPWIDGET_ID)) { + if (intent.getExtras() != null) { + updateAppWidget(context, awm, new int[]{intent.getExtras().getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)}, account); + } + } else { + updateAppWidget(context, awm, awm.getAppWidgetIds(new ComponentName(context, StackWidget.class)), account); + } + } + } + } + } + + @Override + public void onDeleted(Context context, int[] appWidgetIds) { + super.onDeleted(context, appWidgetIds); + final SyncManager syncManager = new SyncManager(context); + + for (int appWidgetId : appWidgetIds) { + syncManager.deleteStackWidgetModel(appWidgetId); + } + } + + /** + * Updates UI data of all {@link StackWidget} instances + */ + public static void notifyDatasetChanged(Context context) { + context.sendBroadcast(new Intent(context, StackWidget.class).setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE)); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetConfigurationActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetConfigurationActivity.java new file mode 100644 index 000000000..6b86b50ba --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetConfigurationActivity.java @@ -0,0 +1,62 @@ +package it.niedermann.nextcloud.deck.ui.widget.stack; + +import android.appwidget.AppWidgetManager; +import android.content.Intent; +import android.os.Bundle; + +import androidx.appcompat.app.ActionBar; + +import it.niedermann.nextcloud.deck.DeckLog; +import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.ui.PickStackActivity; + +public class StackWidgetConfigurationActivity extends PickStackActivity { + private int appWidgetId; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + final ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle("Add stack widget"); + } + + setResult(RESULT_CANCELED); + final Bundle extras = getIntent().getExtras(); + + if (extras != null) { + appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, + AppWidgetManager.INVALID_APPWIDGET_ID); + } + + if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) { + DeckLog.error("INVALID_APPWIDGET_ID"); + finish(); + } + } + + @Override + protected void onSubmit(Account account, long boardId, long stackId) { + final Bundle extras = new Bundle(); + + syncManager.addStackWidget(appWidgetId, account.getId(), stackId, false); + Intent updateIntent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE, null, + getApplicationContext(), StackWidget.class); + extras.putSerializable(StackWidget.ACCOUNT_KEY, account); + extras.putInt(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); + + // The `extras` bundle is added to the intent this way because using putExtras(extras) + // would have the OS attempt to reassemle the data and cause a crash + // when it finds classes that are only known to this application. + updateIntent.putExtra(StackWidget.BUNDLE_KEY, extras); + setResult(RESULT_OK, updateIntent); + getApplicationContext().sendBroadcast(updateIntent); + + finish(); + } + + @Override + protected boolean showBoardsWithoutEditPermission() { + return true; + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetFactory.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetFactory.java new file mode 100644 index 000000000..87f357e20 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetFactory.java @@ -0,0 +1,134 @@ +package it.niedermann.nextcloud.deck.ui.widget.stack; + +import android.appwidget.AppWidgetManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.widget.RemoteViews; +import android.widget.RemoteViewsService; + +import androidx.lifecycle.LiveData; + +import java.util.List; + +import it.niedermann.nextcloud.deck.DeckLog; +import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.model.full.FullBoard; +import it.niedermann.nextcloud.deck.model.full.FullCard; +import it.niedermann.nextcloud.deck.model.full.FullStack; +import it.niedermann.nextcloud.deck.persistence.sync.SyncManager; +import it.niedermann.nextcloud.deck.ui.card.EditActivity; + +public class StackWidgetFactory implements RemoteViewsService.RemoteViewsFactory { + private final Context context; + private final int appWidgetId; + private final long accountId; + private final long stackId; + + private Account account; + private FullStack stack; + private List<FullCard> cardList; + + StackWidgetFactory(Context context, Intent intent) { + this.context = context; + appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, + AppWidgetManager.INVALID_APPWIDGET_ID); + accountId = intent.getLongExtra(StackWidget.ACCOUNT_ID_KEY + appWidgetId, -1); + stackId = intent.getLongExtra(StackWidget.STACK_ID_KEY + appWidgetId, -1); + if (intent.hasExtra(StackWidget.BUNDLE_KEY + appWidgetId)) { + account = (Account) intent.getBundleExtra(StackWidget.BUNDLE_KEY + appWidgetId).getSerializable(StackWidget.ACCOUNT_KEY + appWidgetId); + } + } + + @Override + public void onCreate() { + SyncManager syncManager = new SyncManager(context); + + LiveData<FullStack> stackLiveData = syncManager.getStack(accountId, stackId); + stackLiveData.observeForever((FullStack fullStack) -> { + if (fullStack != null) { + RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_stack); + stack = fullStack; + views.setTextViewText(R.id.widget_stack_title_tv, stack.getStack().getTitle()); + + LiveData<FullBoard> fullBoardLiveData = syncManager.getFullBoardById(accountId, stack.getStack().getBoardId()); + fullBoardLiveData.observeForever((FullBoard fullBoard) -> { + if (fullBoard != null) { + views.setInt(R.id.widget_stack_header_icon, "setColorFilter", fullBoard.getBoard().getColor()); + notifyAppWidgetUpdate(views); + } + }); + + LiveData<List<FullCard>> fullCardData = syncManager.getFullCardsForStack(accountId, stackId, null); + fullCardData.observeForever((List<FullCard> fullCards) -> cardList = fullCards); + notifyAppWidgetUpdate(views); + } + }); + } + + @Override + public void onDataSetChanged() { + + } + + + @Override + public void onDestroy() { + + } + + @Override + public int getCount() { + return stack == null ? 0 : stack.getCards().size(); + } + + @Override + public RemoteViews getViewAt(int i) { + RemoteViews widget_entry; + + if (cardList == null || i > (cardList.size() - 1) || cardList.get(i) == null) { + DeckLog.error("Card not found at position " + i); + return null; + } + + FullCard card = cardList.get(i); + + widget_entry = new RemoteViews(context.getPackageName(), R.layout.widget_stack_entry); + widget_entry.setTextViewText(R.id.widget_entry_content_tv, card.card.getTitle()); + + final Intent intent = EditActivity.createEditCardIntent(context, account, stack.getStack().getBoardId(), card.getCard().getLocalId()); + intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME))); + widget_entry.setOnClickFillInIntent(R.id.widget_stack_entry, intent); + + return widget_entry; + } + + @Override + public RemoteViews getLoadingView() { + return null; + } + + @Override + public int getViewTypeCount() { + return 1; + } + + @Override + public long getItemId(int i) { + return i; + } + + @Override + public boolean hasStableIds() { + return true; + } + + private void notifyAppWidgetUpdate(RemoteViews views) { + AppWidgetManager awm = AppWidgetManager.getInstance(context); + int[] appWidgetIds = awm.getAppWidgetIds(new ComponentName(context, StackWidget.class)); + awm.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.stack_widget_lv); + awm.updateAppWidget(appWidgetId, views); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetService.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetService.java new file mode 100644 index 000000000..9299a96e2 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/widget/stack/StackWidgetService.java @@ -0,0 +1,11 @@ +package it.niedermann.nextcloud.deck.ui.widget.stack; + +import android.content.Intent; +import android.widget.RemoteViewsService; + +public class StackWidgetService extends RemoteViewsService { + @Override + public RemoteViewsFactory onGetViewFactory(Intent intent) { + return new StackWidgetFactory(this.getApplicationContext(), intent); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/AttachmentUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/AttachmentUtil.java index ce9e10012..3931e3323 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/util/AttachmentUtil.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/util/AttachmentUtil.java @@ -1,9 +1,12 @@ package it.niedermann.nextcloud.deck.util; import android.content.Context; +import android.content.Intent; import android.net.Uri; +import android.widget.Toast; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import java.io.File; import java.io.FileNotFoundException; @@ -12,6 +15,8 @@ import java.io.IOException; import java.io.InputStream; import it.niedermann.nextcloud.deck.DeckLog; +import it.niedermann.nextcloud.deck.R; +import it.niedermann.nextcloud.deck.model.Attachment; /** * Created by stefan on 07.03.20. @@ -22,7 +27,34 @@ public class AttachmentUtil { private AttachmentUtil() { } - public static String getRemoteUrl(String accountUrl, long cardRemoteId, long attachmentRemoteId) { + /** + * @return {@link AttachmentUtil#getRemoteUrl} or {@link Attachment#getLocalPath()} as fallback in case this {@param attachment} has not yet been synced. + */ + @Nullable + public static String getRemoteOrLocalUrl(@NonNull String accountUrl, @Nullable Long cardRemoteId, @NonNull Attachment attachment) { + return (attachment.getId() == null || cardRemoteId == null) + ? attachment.getLocalPath() + : getRemoteUrl(accountUrl, cardRemoteId, attachment.getId()); + } + + /** + * Tries to open the given {@link Attachment} in web browser. Displays a toast on failure. + */ + public static void openAttachmentInBrowser(@NonNull Context context, @NonNull String accountUrl, Long cardRemoteId, Long attachmentRemoteId) { + if (cardRemoteId == null) { + Toast.makeText(context, R.string.card_does_not_yet_exist, Toast.LENGTH_LONG).show(); + DeckLog.logError(new IllegalArgumentException("cardRemoteId must not be null.")); + return; + } + if (attachmentRemoteId == null) { + Toast.makeText(context, R.string.attachment_does_not_yet_exist, Toast.LENGTH_LONG).show(); + DeckLog.logError(new IllegalArgumentException("attachmentRemoteId must not be null.")); + return; + } + context.startActivity(new Intent(Intent.ACTION_VIEW).setData(Uri.parse(AttachmentUtil.getRemoteUrl(accountUrl, cardRemoteId, attachmentRemoteId)))); + } + + private static String getRemoteUrl(@NonNull String accountUrl, @NonNull Long cardRemoteId, @NonNull Long attachmentRemoteId) { return accountUrl + "/index.php/apps/deck/cards/" + cardRemoteId + "/attachment/" + attachmentRemoteId; } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/ClipboardUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/ClipboardUtil.java deleted file mode 100644 index 1dad224e9..000000000 --- a/app/src/main/java/it/niedermann/nextcloud/deck/util/ClipboardUtil.java +++ /dev/null @@ -1,38 +0,0 @@ -package it.niedermann.nextcloud.deck.util; - -import android.content.ClipData; -import android.content.ClipboardManager; -import android.content.Context; -import android.widget.Toast; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import it.niedermann.nextcloud.deck.DeckLog; -import it.niedermann.nextcloud.deck.R; - -import static android.content.Context.CLIPBOARD_SERVICE; - -public class ClipboardUtil { - - private ClipboardUtil() { - } - - public static boolean copyToClipboard(@NonNull Context context, @Nullable String text) { - return copyToClipboard(context, text, text); - } - - public static boolean copyToClipboard(@NonNull Context context, @Nullable String label, @Nullable String text) { - final ClipboardManager clipboardManager = (ClipboardManager) context.getSystemService(CLIPBOARD_SERVICE); - if (clipboardManager == null) { - DeckLog.error("ClipboardManager is null"); - Toast.makeText(context, R.string.could_not_copy_to_clipboard, Toast.LENGTH_LONG).show(); - return false; - } - final ClipData clipData = ClipData.newPlainText(label, text); - clipboardManager.setPrimaryClip(clipData); - DeckLog.info("Copied to clipboard: [" + label + "] \"" + text + "\""); - Toast.makeText(context, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show(); - return true; - } -} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/ColorUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/ColorUtil.java deleted file mode 100644 index 08354c5fb..000000000 --- a/app/src/main/java/it/niedermann/nextcloud/deck/util/ColorUtil.java +++ /dev/null @@ -1,165 +0,0 @@ -package it.niedermann.nextcloud.deck.util; - -import android.graphics.Color; - -import androidx.annotation.ColorInt; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.util.Pair; - -import java.util.HashMap; -import java.util.Map; - -public final class ColorUtil { - - private static final Map<ColorPair, Boolean> CONTRAST_RATIO_SUFFICIENT_CACHE = new HashMap<>(); - private static final Map<Integer, Integer> FOREGROUND_CACHE = new HashMap<>(); - private static final Map<Integer, Boolean> IS_DARK_COLOR_CACHE = new HashMap<>(); - - private ColorUtil() { - } - - @ColorInt - public static int getForegroundColorForBackgroundColor(@ColorInt int color) { - Integer ret = FOREGROUND_CACHE.get(color); - if (ret == null) { - if (Color.TRANSPARENT == color) - ret = Color.BLACK; - else if (isColorDark(color)) - ret = Color.WHITE; - else - ret = Color.BLACK; - - FOREGROUND_CACHE.put(color, ret); - } - return ret; - } - - /** - * @return well formatted string starting with a hash followed by 6 hex numbers that is parsable by {@link Color#parseColor(String)}. - */ - public static String formatColorToParsableHexString(String input) { - if (input == null) { - throw new IllegalArgumentException("input color string is null"); - } - if (isParsableValidHexColorString(input)) { - return input; - } - final char[] chars = input.replaceAll("#", "").toCharArray(); - final StringBuilder sb = new StringBuilder(7).append("#"); - if (chars.length == 6) { - sb.append(chars); - } else if (chars.length == 3) { - for (char c : chars) { - sb.append(c).append(c); - } - } else { - throw new IllegalArgumentException("unparsable color string: \"" + input + "\""); - } - final String formattedHexColor = sb.toString(); - if (isParsableValidHexColorString(formattedHexColor)) { - return formattedHexColor; - } else { - throw new IllegalArgumentException("\"" + input + "\" is not a valid color string. Result of tried normalizing: " + formattedHexColor); - } - } - - /** - * Checking for {@link Color#parseColor(String)} being able to parse the input is the important part because we don't know the implementation and rely on it to be able to parse the color. - * - * @return true, if the input starts with a hash followed by 6 characters of hex numbers and is parsable by {@link Color#parseColor(String)}. - */ - private static boolean isParsableValidHexColorString(@NonNull String input) { - try { - Color.parseColor(input); - return input.matches("#[a-fA-F0-9]{6}"); - } catch (Exception e) { - return false; - } - } - - public static boolean isColorDark(@ColorInt int color) { - Boolean ret = IS_DARK_COLOR_CACHE.get(color); - if (ret == null) { - ret = getBrightness(color) < 200; - IS_DARK_COLOR_CACHE.put(color, ret); - } - return ret; - } - - private static int getBrightness(@ColorInt int color) { - final int[] rgb = {Color.red(color), Color.green(color), Color.blue(color)}; - - return (int) Math.sqrt(rgb[0] * rgb[0] * .241 + rgb[1] - * rgb[1] * .691 + rgb[2] * rgb[2] * .068); - } - - // --------------------------------------------------- - // Based on https://github.com/LeaVerou/contrast-ratio - // --------------------------------------------------- - - public static boolean contrastRatioIsSufficient(@ColorInt int colorOne, @ColorInt int colorTwo) { - ColorPair key = new ColorPair(colorOne, colorTwo); - Boolean ret = CONTRAST_RATIO_SUFFICIENT_CACHE.get(key); - if (ret == null) { - ret = getContrastRatio(colorOne, colorTwo) > 3d; - CONTRAST_RATIO_SUFFICIENT_CACHE.put(key, ret); - return ret; - } - return ret; - } - - public static boolean contrastRatioIsSufficientBigAreas(@ColorInt int colorOne, @ColorInt int colorTwo) { - ColorPair key = new ColorPair(colorOne, colorTwo); - Boolean ret = CONTRAST_RATIO_SUFFICIENT_CACHE.get(key); - if (ret == null) { - ret = getContrastRatio(colorOne, colorTwo) > 1.47d; - CONTRAST_RATIO_SUFFICIENT_CACHE.put(key, ret); - return ret; - } - return ret; - } - - public static double getContrastRatio(@ColorInt int colorOne, @ColorInt int colorTwo) { - final double lum1 = getLuminanace(colorOne); - final double lum2 = getLuminanace(colorTwo); - final double brightest = Math.max(lum1, lum2); - final double darkest = Math.min(lum1, lum2); - return (brightest + 0.05) / (darkest + 0.05); - } - - private static double getLuminanace(@ColorInt int color) { - final int[] rgb = {Color.red(color), Color.green(color), Color.blue(color)}; - return getSubcolorLuminance(rgb[0]) * 0.2126 + getSubcolorLuminance(rgb[1]) * 0.7152 + getSubcolorLuminance(rgb[2]) * 0.0722; - } - - private static double getSubcolorLuminance(@ColorInt int color) { - final double value = color / 255d; - return value <= 0.03928 - ? value / 12.92 - : Math.pow((value + 0.055) / 1.055, 2.4); - } - - private static class ColorPair extends Pair<Integer, Integer> { - - private ColorPair(@Nullable Integer first, @Nullable Integer second) { - super(first, second); - } - - @SuppressWarnings({"EqualsWhichDoesntCheckParameterClass", "NumberEquality"}) - @Override - public boolean equals(Object o) { - final ColorPair colorPair = (ColorPair) o; - if (first != colorPair.first) return false; - return second == colorPair.second; - } - - @SuppressWarnings("ConstantConditions") - @Override - public int hashCode() { - int result = first; - result = 31 * result + second; - return result; - } - } -} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/DeckColorUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/DeckColorUtil.java new file mode 100644 index 000000000..e01d0ec29 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/util/DeckColorUtil.java @@ -0,0 +1,59 @@ +package it.niedermann.nextcloud.deck.util; + +import androidx.annotation.ColorInt; +import androidx.annotation.Nullable; +import androidx.core.util.Pair; + +import java.util.HashMap; +import java.util.Map; + +import it.niedermann.android.util.ColorUtil; + +public final class DeckColorUtil { + + private static final Map<ColorPair, Boolean> CONTRAST_RATIO_SUFFICIENT_CACHE = new HashMap<>(); + + public static boolean contrastRatioIsSufficient(@ColorInt int colorOne, @ColorInt int colorTwo) { + ColorPair key = new ColorPair(colorOne, colorTwo); + Boolean ret = CONTRAST_RATIO_SUFFICIENT_CACHE.get(key); + if (ret == null) { + ret = ColorUtil.INSTANCE.getContrastRatio(colorOne, colorTwo) > 3d; + CONTRAST_RATIO_SUFFICIENT_CACHE.put(key, ret); + return ret; + } + return ret; + } + + public static boolean contrastRatioIsSufficientBigAreas(@ColorInt int colorOne, @ColorInt int colorTwo) { + ColorPair key = new ColorPair(colorOne, colorTwo); + Boolean ret = CONTRAST_RATIO_SUFFICIENT_CACHE.get(key); + if (ret == null) { + ret = ColorUtil.INSTANCE.getContrastRatio(colorOne, colorTwo) > 1.47d; + CONTRAST_RATIO_SUFFICIENT_CACHE.put(key, ret); + return ret; + } + return ret; + } + + private static class ColorPair extends Pair<Integer, Integer> { + + private ColorPair(@Nullable Integer first, @Nullable Integer second) { + super(first, second); + } + + @SuppressWarnings({"EqualsWhichDoesntCheckParameterClass", "NumberEquality"}) + @Override + public boolean equals(Object o) { + final ColorPair colorPair = (ColorPair) o; + if (first != colorPair.first) return false; + return second == colorPair.second; + } + + @Override + public int hashCode() { + int result = first; + result = 31 * result + second; + return result; + } + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/DimensionUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/DimensionUtil.java deleted file mode 100644 index 6ff22eace..000000000 --- a/app/src/main/java/it/niedermann/nextcloud/deck/util/DimensionUtil.java +++ /dev/null @@ -1,17 +0,0 @@ -package it.niedermann.nextcloud.deck.util; - -import android.content.Context; - -import androidx.annotation.DimenRes; -import androidx.annotation.NonNull; -import androidx.annotation.Px; - -public final class DimensionUtil { - private DimensionUtil() { - } - - @Px - public static int dpToPx(@NonNull Context context, @DimenRes int resource) { - return context.getResources().getDimensionPixelSize(resource); - } -} 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..1f6490bf2 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 @@ -38,7 +38,7 @@ public class DrawerMenuUtil { SubMenu boardsMenu = menu.addSubMenu(R.string.simple_boards); int index = 0; for (Board board : boards) { - MenuItem m = boardsMenu.add(Menu.NONE, index++, Menu.NONE, board.getTitle()).setIcon(ViewUtil.getTintedImageView(context, R.drawable.circle_grey600_36dp, "#" + board.getColor())); + MenuItem m = boardsMenu.add(Menu.NONE, index++, Menu.NONE, board.getTitle()).setIcon(ViewUtil.getTintedImageView(context, R.drawable.circle_grey600_36dp, String.format("#%06X", (0xFFFFFF & board.getColor())))); if (currentServerVersionIsSupported) { if (board.isPermissionManage()) { AppCompatImageButton contextMenu = new AppCompatImageButton(context); @@ -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/java/it/niedermann/nextcloud/deck/util/ExceptionUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/ExceptionUtil.java deleted file mode 100644 index 8599f0b6d..000000000 --- a/app/src/main/java/it/niedermann/nextcloud/deck/util/ExceptionUtil.java +++ /dev/null @@ -1,81 +0,0 @@ -package it.niedermann.nextcloud.deck.util; - -import android.content.Context; -import android.content.pm.PackageManager; -import android.os.Build; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.UiThread; - -import com.nextcloud.android.sso.exceptions.NextcloudFilesAppNotInstalledException; -import com.nextcloud.android.sso.helper.VersionCheckHelper; -import com.nextcloud.android.sso.ui.UiExceptionManager; - -import java.io.PrintWriter; -import java.io.StringWriter; - -import it.niedermann.nextcloud.deck.BuildConfig; -import it.niedermann.nextcloud.deck.DeckLog; -import it.niedermann.nextcloud.deck.model.Account; - -public class ExceptionUtil { - - private ExceptionUtil() { - - } - - public static String getDebugInfos(@NonNull Context context, Throwable throwable, @Nullable Account account) { - return "" + - getAppVersions(context, account) + - "\n\n---\n" + - getDeviceInfos() + - "\n\n---" + - "\n\n" + - getStacktraceOf(throwable); - } - - private static String getAppVersions(Context context, @Nullable Account account) { - String versions = "" - + "App Version: " + BuildConfig.VERSION_NAME + "\n" - + "App Version Code: " + BuildConfig.VERSION_CODE + "\n" - + "App Flavor: " + BuildConfig.FLAVOR + "\n"; - - if (account != null) { - versions += "\n"; - versions += "Deck Server Version: " + account.getServerDeckVersion() + "\n"; - } - - versions += "\n"; - try { - versions += "Files App Version Code: " + VersionCheckHelper.getNextcloudFilesVersionCode(context); - } catch (PackageManager.NameNotFoundException e) { - versions += "Files App Version Code: " + e.getMessage(); - e.printStackTrace(); - } - return versions; - } - - private static String getDeviceInfos() { - return "" - + "\nOS Version: " + System.getProperty("os.version") + "(" + Build.VERSION.INCREMENTAL + ")" - + "\nOS API Level: " + Build.VERSION.SDK_INT - + "\nDevice: " + Build.DEVICE - + "\nManufacturer: " + Build.MANUFACTURER - + "\nModel (and Product): " + Build.MODEL + " (" + Build.PRODUCT + ")"; - } - - private static String getStacktraceOf(Throwable e) { - StringWriter sw = new StringWriter(); - e.printStackTrace(new PrintWriter(sw)); - return sw.toString(); - } - - @UiThread - public static void handleNextcloudFilesAppNotInstalledException(@NonNull Context context, @NonNull NextcloudFilesAppNotInstalledException exception) { - UiExceptionManager.showDialogForException(context, exception); - DeckLog.warn("============================================================="); - DeckLog.warn("Nextcloud app is not installed. Cannot choose account"); - exception.printStackTrace(); - } -} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/ProjectUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/ProjectUtil.java new file mode 100644 index 000000000..015241eef --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/util/ProjectUtil.java @@ -0,0 +1,87 @@ +package it.niedermann.nextcloud.deck.util; + +import android.net.Uri; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.net.URL; + +import it.niedermann.nextcloud.deck.model.Account; + +public class ProjectUtil { + + private ProjectUtil() { + } + + @NonNull + public static Uri getResourceUri(@NonNull Account account, @NonNull String link) throws IllegalArgumentException { + try { + // Assume link contains a fully qualified Uri including host + final URL u = new URL(link); + return Uri.parse(u.toString()); + } catch (Throwable linkIsNotQualified) { + try { + // Assume link is a absolute path that needs to be concatenated with account url for a complete Uri + final URL u = new URL(account.getUrl() + link); + return Uri.parse(u.toString()); + } catch (Throwable throwable) { + throw new IllegalArgumentException("Could not parse " + Uri.class.getSimpleName() + ": " + link, throwable); + } + } + } + + /** + * extracts the values of board- and card-ID from url. + * Depending on what kind of url it gets, it will return a long[] of lenght 1 or 2: + * If the url contains both values, you'll get 2, if it contains only the board, you'll get 1. + * <p> + * The order is fixed here: [boardId, cardId] + * + * @param url to extract from + * @return extracted and parsed values as long[] with length 1-2 + */ + public static long[] extractBoardIdAndCardIdFromUrl(@Nullable String url) throws IllegalArgumentException { + if (url == null) { + throw new IllegalArgumentException("provided url is null"); + } + url = url.trim(); + // extract important part + String[] splitByPrefix = url.split(".*index\\.php/apps/deck/#/board/"); + // split into board- and card part + if (splitByPrefix.length < 2) { + throw new IllegalArgumentException("this doesn't seem to be an URL containing the board ID"); + } + String[] splitBySeparator = splitByPrefix[1].split("/card/"); + + // remove any unexpected stuff + if (splitBySeparator.length > 1 && splitBySeparator[1].contains("/")) { + splitBySeparator[1] = splitBySeparator[1].split("/")[0]; + } + if (splitBySeparator.length > 0 && splitBySeparator[0].contains("/")) { + splitBySeparator[0] = splitBySeparator[0].split("/")[0]; + } + + if (splitBySeparator.length < 1) { + throw new IllegalArgumentException("this doesn't seem to be a valid URL containing the board ID"); + } + + // return result + long boardId = Long.parseLong(splitBySeparator[0]); + if (boardId < 1) { + throw new IllegalArgumentException("invalid boardId: "+boardId); + } + if (splitBySeparator.length == 1) { + return new long[]{boardId}; + } else if (splitBySeparator.length == 2) { + long cardId = Long.parseLong(splitBySeparator[1]); + if (cardId > 0) { + return new long[]{boardId, cardId}; + } else { + return new long[]{boardId}; + } + } else { + throw new IllegalArgumentException("could not parse URL for board- and/or card-ID"); + } + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/util/ViewUtil.java b/app/src/main/java/it/niedermann/nextcloud/deck/util/ViewUtil.java index 6abbde557..a52053ec1 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/util/ViewUtil.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/util/ViewUtil.java @@ -16,6 +16,7 @@ import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.Px; +import androidx.core.content.ContextCompat; import androidx.core.graphics.drawable.DrawableCompat; import androidx.core.widget.TextViewCompat; @@ -27,6 +28,7 @@ import com.bumptech.glide.request.transition.Transition; import java.util.Date; import java.util.List; +import it.niedermann.android.util.DimensionUtil; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.model.ocs.comment.Mention; @@ -38,13 +40,14 @@ public final class ViewUtil { } public static void addAvatar(@NonNull ImageView avatar, @NonNull String baseUrl, @NonNull String userId, @DrawableRes int errorResource) { - addAvatar(avatar, baseUrl, userId, DimensionUtil.dpToPx(avatar.getContext(), R.dimen.avatar_size), errorResource); + addAvatar(avatar, baseUrl, userId, DimensionUtil.INSTANCE.dpToPx(avatar.getContext(), R.dimen.avatar_size), errorResource); } public static void addAvatar(@NonNull ImageView avatar, @NonNull String baseUrl, @NonNull String userId, @Px int avatarSizeInPx, @DrawableRes int errorResource) { final String uri = baseUrl + "/index.php/avatar/" + Uri.encode(userId) + "/" + avatarSizeInPx; Glide.with(avatar.getContext()) .load(uri) + .placeholder(errorResource) .error(errorResource) .apply(RequestOptions.circleCropTransform()) .into(avatar); @@ -69,12 +72,13 @@ public final class ViewUtil { } cardDueDate.setBackgroundResource(backgroundDrawable); - cardDueDate.setTextColor(context.getResources().getColor(textColor)); - TextViewCompat.setCompoundDrawableTintList(cardDueDate, ColorStateList.valueOf(context.getResources().getColor(textColor))); + cardDueDate.setTextColor(ContextCompat.getColor(context, textColor)); + TextViewCompat.setCompoundDrawableTintList(cardDueDate, ColorStateList.valueOf(ContextCompat.getColor(context, textColor))); } public static Drawable getTintedImageView(@NonNull Context context, @DrawableRes int imageId, @NonNull String color) { - final Drawable drawable = context.getResources().getDrawable(imageId); + final Drawable drawable = ContextCompat.getDrawable(context, imageId); + assert drawable != null; final Drawable wrapped = DrawableCompat.wrap(drawable).mutate(); DrawableCompat.setTint(wrapped, Color.parseColor(color)); return drawable; @@ -118,7 +122,7 @@ public final class ViewUtil { Glide.with(context) .asBitmap() .placeholder(R.drawable.ic_person_grey600_24dp) - .load(account.getUrl() + "/index.php/avatar/" + messageBuilder.subSequence(spanStart + 1, spanEnd).toString() + "/" + DimensionUtil.dpToPx(context, R.dimen.icon_size_details)) + .load(account.getUrl() + "/index.php/avatar/" + messageBuilder.subSequence(spanStart + 1, spanEnd).toString() + "/" + DimensionUtil.INSTANCE.dpToPx(context, R.dimen.icon_size_details)) .apply(RequestOptions.circleCropTransform()) .into(new CustomTarget<Bitmap>() { @Override diff --git a/app/src/main/res/drawable/ic_arrow_drop_down_black_24dp.xml b/app/src/main/res/drawable/ic_arrow_drop_down_black_24dp.xml deleted file mode 100644 index 95b52e8bd..000000000 --- a/app/src/main/res/drawable/ic_arrow_drop_down_black_24dp.xml +++ /dev/null @@ -1,5 +0,0 @@ -<vector android:autoMirrored="true" android:height="24dp" - android:tint="#FFFFFF" android:viewportHeight="24.0" - android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> - <path android:fillColor="#FF000000" android:pathData="M7,10l5,5 5,-5z"/> -</vector> diff --git a/app/src/main/res/drawable/ic_baseline_block_24.xml b/app/src/main/res/drawable/ic_baseline_block_24.xml new file mode 100644 index 000000000..26080c324 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_block_24.xml @@ -0,0 +1,5 @@ +<vector android:height="@dimen/avatar_size" android:tint="#757575" + android:viewportHeight="24" android:viewportWidth="24" + android:width="@dimen/avatar_size" xmlns:android="http://schemas.android.com/apk/res/android"> + <path android:fillColor="@android:color/white" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM4,12c0,-4.42 3.58,-8 8,-8 1.85,0 3.55,0.63 4.9,1.69L5.69,16.9C4.63,15.55 4,13.85 4,12zM12,20c-1.85,0 -3.55,-0.63 -4.9,-1.69L18.31,7.1C19.37,8.45 20,10.15 20,12c0,4.42 -3.58,8 -8,8z"/> +</vector> 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/drawable/ic_baseline_mention_24.xml b/app/src/main/res/drawable/ic_baseline_mention_24.xml new file mode 100644 index 000000000..32c6f57e9 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_mention_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="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10h5v-2h-5c-4.34,0 -8,-3.66 -8,-8s3.66,-8 8,-8 8,3.66 8,8v1.43c0,0.79 -0.71,1.57 -1.5,1.57s-1.5,-0.78 -1.5,-1.57L17,12c0,-2.76 -2.24,-5 -5,-5s-5,2.24 -5,5 2.24,5 5,5c1.38,0 2.64,-0.56 3.54,-1.47 0.65,0.89 1.77,1.47 2.96,1.47 1.97,0 3.5,-1.6 3.5,-3.57L22,12c0,-5.52 -4.48,-10 -10,-10zM12,15c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_baseline_subject_24.xml b/app/src/main/res/drawable/ic_baseline_subject_24.xml new file mode 100644 index 000000000..37cd0c43d --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_subject_24.xml @@ -0,0 +1,5 @@ +<vector android:autoMirrored="true" android:height="24dp" + android:tint="#757575" android:viewportHeight="24" + android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> + <path android:fillColor="#FF000000" android:pathData="M14,17L4,17v2h10v-2zM20,9L4,9v2h16L20,9zM4,15h16v-2L4,13v2zM4,5v2h16L20,5L4,5z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_filter_list_active_white_24dp.xml b/app/src/main/res/drawable/ic_filter_list_active_white_24dp.xml deleted file mode 100644 index 39225dc3c..000000000 --- a/app/src/main/res/drawable/ic_filter_list_active_white_24dp.xml +++ /dev/null @@ -1,8 +0,0 @@ -<vector android:autoMirrored="true" android:height="24dp" - android:tint="#FFFFFF" android:viewportHeight="24.0" - android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> - <path android:fillColor="#FF000000" android:pathData="M10,18h4v-2h-4v2zM3,6v2h18L21,6L3,6zM6,13h12v-2L6,11v2z"/> - <path - android:fillColor="#ffffff" - android:pathData="M20,20m-4,0a4,4 0,1 1,8 0a4,4 0,1 1,-8 0"/> -</vector> diff --git a/app/src/main/res/drawable/ic_format_align_left_black_24dp.xml b/app/src/main/res/drawable/ic_format_align_left_black_24dp.xml deleted file mode 100644 index b676ff3e8..000000000 --- a/app/src/main/res/drawable/ic_format_align_left_black_24dp.xml +++ /dev/null @@ -1,11 +0,0 @@ -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" - android:height="24dp" - android:autoMirrored="true" - android:tint="#757575" - android:viewportWidth="24" - android:viewportHeight="24"> - <path - android:fillColor="#FF000000" - android:pathData="M15,15L3,15v2h12v-2zM15,7L3,7v2h12L15,7zM3,13h18v-2L3,11v2zM3,21h18v-2L3,19v2zM3,3v2h18L21,3L3,3z" /> -</vector> diff --git a/app/src/main/res/drawable/ic_more_horiz_black_24dp.xml b/app/src/main/res/drawable/ic_more_horiz_black_24dp.xml deleted file mode 100644 index 817e37227..000000000 --- a/app/src/main/res/drawable/ic_more_horiz_black_24dp.xml +++ /dev/null @@ -1,5 +0,0 @@ -<vector android:autoMirrored="true" android:height="24dp" - android:tint="#000000" android:viewportHeight="24.0" - android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> - <path android:fillColor="#FF000000" android:pathData="M6,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM18,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z"/> -</vector> diff --git a/app/src/main/res/drawable/ic_projects_24.xml b/app/src/main/res/drawable/ic_projects_24.xml new file mode 100644 index 000000000..8fb5da418 --- /dev/null +++ b/app/src/main/res/drawable/ic_projects_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="M13,13v8h8v-8h-8zM3,21h8v-8L3,13v8zM3,3v8h8L11,3L3,3zM16.66,1.69L11,7.34 16.66,13l5.66,-5.66 -5.66,-5.65z"/> +</vector> diff --git a/app/src/main/res/drawable/project_deck_36dp.xml b/app/src/main/res/drawable/project_deck_36dp.xml new file mode 100644 index 000000000..4a9468e4e --- /dev/null +++ b/app/src/main/res/drawable/project_deck_36dp.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:tint="#757575" + android:viewportWidth="16" + android:viewportHeight="16"> + <group> + <path + android:fillColor="#fff" + android:pathData="M2,7L14,7A1,1 0,0 1,15 8L15,14A1,1 0,0 1,14 15L2,15A1,1 0,0 1,1 14L1,8A1,1 0,0 1,2 7z" /> + <path + android:fillColor="#fff" + android:pathData="M2.5,5L13.5,5A0.5,0.5 0,0 1,14 5.5L14,5.5A0.5,0.5 0,0 1,13.5 6L2.5,6A0.5,0.5 0,0 1,2 5.5L2,5.5A0.5,0.5 0,0 1,2.5 5z" /> + <path + android:fillColor="#fff" + android:pathData="M3.5,3L12.5,3A0.5,0.5 0,0 1,13 3.5L13,3.5A0.5,0.5 0,0 1,12.5 4L3.5,4A0.5,0.5 0,0 1,3 3.5L3,3.5A0.5,0.5 0,0 1,3.5 3z" /> + <path + android:fillColor="#fff" + android:pathData="M4.5,1L11.5,1A0.5,0.5 0,0 1,12 1.5L12,1.5A0.5,0.5 0,0 1,11.5 2L4.5,2A0.5,0.5 0,0 1,4 1.5L4,1.5A0.5,0.5 0,0 1,4.5 1z" /> + </group> +</vector> diff --git a/app/src/main/res/drawable/project_file_36dp.xml b/app/src/main/res/drawable/project_file_36dp.xml new file mode 100644 index 000000000..8d025588c --- /dev/null +++ b/app/src/main/res/drawable/project_file_36dp.xml @@ -0,0 +1,5 @@ +<vector android:autoMirrored="false" android:height="36dp" + android:tint="#757575" android:viewportHeight="36" + android:viewportWidth="36" android:width="36dp" xmlns:android="http://schemas.android.com/apk/res/android"> + <path android:fillColor="@android:color/white" android:pathData="m3 4c-0.5 0-1 0.5-1 1v22c0 0.52 0.48 1 1 1h26c0.52 0 1-0.482 1-1v-18c0-0.5-0.5-1-1-1h-13l-4-4z"/> +</vector>
\ No newline at end of file diff --git a/app/src/main/res/drawable/project_talk_36dp.xml b/app/src/main/res/drawable/project_talk_36dp.xml new file mode 100644 index 000000000..46fe60de5 --- /dev/null +++ b/app/src/main/res/drawable/project_talk_36dp.xml @@ -0,0 +1,11 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="16dp" + android:height="16dp" + android:autoMirrored="false" + android:tint="#757575" + android:viewportWidth="16" + android:viewportHeight="16"> + <path + android:fillColor="@android:color/white" + android:pathData="m7.9992 0.999a6.9993 6.9994 0 0 0 -6.9992 6.9996 6.9993 6.9994 0 0 0 6.9992 6.9994 6.9993 6.9994 0 0 0 3.6308 -1.024c0.86024 0.34184 2.7871 1.356 3.2457 0.91794 0.47922-0.45765-0.56261-2.6116-0.81238-3.412a6.9993 6.9994 0 0 0 0.935 -3.4814 6.9993 6.9994 0 0 0 -6.9991 -6.9993zm0.0008 2.6611a4.34 4.3401 0 0 1 4.34 4.3401 4.34 4.3401 0 0 1 -4.34 4.3398 4.34 4.3401 0 0 1 -4.34 -4.3398 4.34 4.3401 0 0 1 4.34 -4.3401z" /> +</vector>
\ No newline at end of file diff --git a/app/src/main/res/drawable/widget_stack_preview.png b/app/src/main/res/drawable/widget_stack_preview.png Binary files differnew file mode 100644 index 000000000..442c78fbe --- /dev/null +++ b/app/src/main/res/drawable/widget_stack_preview.png 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/activity_exception.xml b/app/src/main/res/layout/activity_exception.xml index 881c8fa85..932fa7bf2 100644 --- a/app/src/main/res/layout/activity_exception.xml +++ b/app/src/main/res/layout/activity_exception.xml @@ -30,7 +30,6 @@ android:layout_weight="1" android:hint="An error appeared." app:drawableEndCompat="@drawable/ic_bug_report_black_24dp" - app:drawableRightCompat="@drawable/ic_bug_report_black_24dp" tools:ignore="HardcodedText" /> </LinearLayout> diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index e0af061bf..ac0dadf82 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -45,7 +45,6 @@ android:paddingEnd="@dimen/spacer_1hx" android:text="@string/info_box_maintenance_mode" android:textColor="@color/grey600" - app:drawableLeftCompat="@drawable/ic_info_outline_grey600_24dp" app:drawableStartCompat="@drawable/ic_info_outline_grey600_24dp" /> </LinearLayout> @@ -71,7 +70,6 @@ android:paddingEnd="@dimen/spacer_1hx" android:text="@string/info_box_version_not_supported" android:textColor="@android:color/white" - app:drawableLeftCompat="@drawable/ic_warning_white_24dp" app:drawableStartCompat="@drawable/ic_warning_white_24dp" /> </LinearLayout> diff --git a/app/src/main/res/layout/activity_prepare_create.xml b/app/src/main/res/layout/activity_pick_stack.xml index 1128f04f4..6ba9b567f 100644 --- a/app/src/main/res/layout/activity_prepare_create.xml +++ b/app/src/main/res/layout/activity_pick_stack.xml @@ -4,7 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:showIn="@layout/activity_prepare_create"> + tools:showIn="@layout/activity_pick_stack"> <com.google.android.material.appbar.AppBarLayout android:id="@+id/appBarLayout" @@ -25,35 +25,10 @@ android:layout_below="@id/appBarLayout" android:padding="@dimen/spacer_2x"> - <LinearLayout + <FrameLayout + android:id="@+id/fragment_container" android:layout_width="match_parent" - 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" - android:spinnerMode="dialog" - 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" - android:spinnerMode="dialog" - 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" - android:spinnerMode="dialog" - tools:listitem="@layout/item_board" /> - </LinearLayout> + android:layout_height="wrap_content" /> </ScrollView> <LinearLayout diff --git a/app/src/main/res/layout/dialog_assignee.xml b/app/src/main/res/layout/dialog_assignee.xml new file mode 100644 index 000000000..6f7157286 --- /dev/null +++ b/app/src/main/res/layout/dialog_assignee.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <ImageView + android:id="@+id/avatar" + android:layout_width="match_parent" + android:layout_height="200dp" + android:contentDescription="@string/user_avatar" + android:scaleType="centerCrop" + android:background="@color/bg_info_box" /> + + <TextView + android:padding="?dialogPreferredPadding" + android:id="@+id/displayName" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textAppearance="?attr/textAppearanceHeadline1" + tools:text="@tools:sample/full_names" /> +</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..c00dcfbf4 --- /dev/null +++ b/app/src/main/res/layout/dialog_move_card.xml @@ -0,0 +1,80 @@ +<?xml version="1.0" encoding="utf-8"?> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:orientation="vertical" + android:paddingStart="@dimen/spacer_2x" + android:paddingTop="@dimen/spacer_2x" + android:paddingEnd="@dimen/spacer_2x" + android:paddingBottom="@dimen/spacer_1x"> + + <TextView + android:id="@+id/title" + style="@style/TextAppearance.AppCompat.Title" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/spacer_1x" + android:layout_marginEnd="@dimen/spacer_1x" + android:ellipsize="end" + android:maxLines="5" + tools:text="@string/action_card_move_title" /> + + <ScrollView + android:id="@+id/scrollView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@id/title" + android:layout_marginTop="@dimen/spacer_2x"> + + <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_below="@id/scrollView" + android:layout_marginTop="@dimen/spacer_2x" + 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:drawableStartCompat="@drawable/ic_warning_white_24dp" + app:drawableTint="@color/danger" + tools:visibility="visible" /> + + <com.google.android.flexbox.FlexboxLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@id/move_warning" + android:layout_marginTop="@dimen/spacer_1x" + android:orientation="horizontal" + app:justifyContent="space_between"> + + <Button + android:id="@+id/cancel" + style="@style/Widget.AppCompat.Button.ButtonBar.AlertDialog" + 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.AppCompat.Button.ButtonBar.AlertDialog" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="end" + android:text="@string/simple_move" + android:textColor="@color/defaultBrand" /> + </com.google.android.flexbox.FlexboxLayout> +</RelativeLayout>
\ No newline at end of file diff --git a/app/src/main/res/layout/dialog_project_resources.xml b/app/src/main/res/layout/dialog_project_resources.xml new file mode 100644 index 000000000..3de37db8b --- /dev/null +++ b/app/src/main/res/layout/dialog_project_resources.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@id/projectName" + android:paddingTop="@dimen/spacer_1x" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" + tools:listitem="@layout/item_project_resource" />
\ No newline at end of file diff --git a/app/src/main/res/layout/fragment_card_edit_tab_comments.xml b/app/src/main/res/layout/fragment_card_edit_tab_comments.xml index 3350480cb..cc5fa77b7 100644 --- a/app/src/main/res/layout/fragment_card_edit_tab_comments.xml +++ b/app/src/main/res/layout/fragment_card_edit_tab_comments.xml @@ -44,9 +44,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="@dimen/spacer_1x" - android:layout_marginLeft="@dimen/spacer_1x" android:layout_marginEnd="@dimen/spacer_1x" - android:layout_marginRight="@dimen/spacer_1x" android:contentDescription="@string/simple_reply" android:padding="@dimen/spacer_1x" app:srcCompat="@drawable/ic_reply_grey600_24dp" /> @@ -57,7 +55,6 @@ android:layout_height="wrap_content" android:layout_gravity="center" android:layout_marginStart="@dimen/spacer_1x" - android:layout_marginLeft="@dimen/spacer_1x" android:layout_weight="1" android:ellipsize="end" android:maxLines="2" @@ -75,6 +72,38 @@ app:srcCompat="@drawable/ic_close_circle_grey600" /> </LinearLayout> + + <LinearLayout + android:id="@+id/mentionProposerWrapper" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center_vertical" + android:paddingStart="@dimen/spacer_2x" + android:paddingTop="@dimen/spacer_1x" + android:paddingEnd="@dimen/spacer_2x" + android:paddingBottom="@dimen/spacer_1x" + android:visibility="gone" + tools:visibility="visible"> + + <ImageView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/spacer_1x" + android:layout_marginEnd="@dimen/spacer_1x" + android:contentDescription="@string/simple_reply" + android:padding="@dimen/spacer_1x" + app:srcCompat="@drawable/ic_baseline_mention_24" /> + + <LinearLayout + android:id="@+id/mention_proposer" + android:layout_width="match_parent" + android:layout_height="@dimen/avatar_size_small" + android:layout_marginStart="@dimen/spacer_1x" + android:layout_marginEnd="@dimen/spacer_1x" + android:gravity="center_vertical" + android:orientation="horizontal" /> + </LinearLayout> + <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" @@ -85,10 +114,11 @@ <ImageView android:layout_width="@dimen/icon_size_details" android:layout_height="match_parent" - android:layout_marginEnd="@dimen/spacer_2x" + android:layout_marginStart="@dimen/spacer_2x" + android:layout_marginEnd="@dimen/spacer_1x" android:contentDescription="@null" - android:tint="@color/grey600" - app:srcCompat="@drawable/ic_comment_white_24dp" /> + app:srcCompat="@drawable/ic_comment_white_24dp" + app:tint="@color/grey600" /> <EditText android:id="@+id/message" diff --git a/app/src/main/res/layout/fragment_card_edit_tab_details.xml b/app/src/main/res/layout/fragment_card_edit_tab_details.xml index b1d52378a..2dfb79889 100644 --- a/app/src/main/res/layout/fragment_card_edit_tab_details.xml +++ b/app/src/main/res/layout/fragment_card_edit_tab_details.xml @@ -1,163 +1,179 @@ <?xml version="1.0" encoding="utf-8"?> <ScrollView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" android:fillViewport="true"> - <LinearLayout xmlns:tools="http://schemas.android.com/tools" + <RelativeLayout android:layout_width="match_parent" - android:layout_height="wrap_content" - android:orientation="vertical" - android:padding="@dimen/spacer_2x"> + android:layout_height="wrap_content"> <LinearLayout + android:id="@+id/details" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginTop="@dimen/spacer_2x" - android:orientation="horizontal"> - - <ImageView - android:layout_width="@dimen/icon_size_details" - android:layout_height="match_parent" - android:layout_marginEnd="@dimen/spacer_2x" - android:contentDescription="@null" - app:srcCompat="@drawable/ic_label_grey600_24dp" /> + android:orientation="vertical" + android:padding="@dimen/spacer_2x"> - <it.niedermann.nextcloud.deck.ui.view.ToggleAutoCompleteTextView - android:id="@+id/labels" + <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" - android:completionThreshold="1" - android:hint="@string/label_labels" - android:inputType="text" /> - </LinearLayout> - - <com.google.android.material.chip.ChipGroup - android:id="@+id/labelsGroup" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginStart="40dp" - android:animateLayoutChanges="true" /> - - <LinearLayout - android:id="@+id/colorPicker" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginTop="@dimen/spacer_2x"> - - - <ImageView - android:layout_width="@dimen/icon_size_details" - android:layout_height="match_parent" - android:layout_marginEnd="@dimen/spacer_2x" - android:contentDescription="@null" - app:srcCompat="@drawable/calendar_blank_grey600_24dp" /> + android:layout_marginTop="@dimen/spacer_2x" + android:orientation="horizontal"> + + <ImageView + android:layout_width="@dimen/icon_size_details" + android:layout_height="match_parent" + android:layout_marginEnd="@dimen/spacer_2x" + android:contentDescription="@null" + app:srcCompat="@drawable/ic_label_grey600_24dp" /> + + <it.niedermann.nextcloud.deck.ui.view.ToggleAutoCompleteTextView + android:id="@+id/labels" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:completionThreshold="1" + android:hint="@string/label_labels" + android:inputType="text" /> + </LinearLayout> + + <com.google.android.material.chip.ChipGroup + android:id="@+id/labelsGroup" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginStart="40dp" + android:animateLayoutChanges="true" /> - <EditText - android:id="@+id/dueDateDate" - android:layout_width="wrap_content" + <LinearLayout + android:id="@+id/colorPicker" + android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_weight="2" - android:enabled="true" - android:focusable="false" - android:hint="@string/hint_due_date_date" - android:importantForAutofill="no" - android:inputType="date" - android:maxLines="1" - tools:text="01/07/2020" /> - - <EditText - android:id="@+id/dueDateTime" - android:layout_width="wrap_content" + android:layout_marginTop="@dimen/spacer_2x"> + + + <ImageView + android:layout_width="@dimen/icon_size_details" + android:layout_height="match_parent" + android:layout_marginEnd="@dimen/spacer_2x" + android:contentDescription="@null" + app:srcCompat="@drawable/calendar_blank_grey600_24dp" /> + + <EditText + android:id="@+id/dueDateDate" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="2" + android:enabled="true" + android:focusable="false" + android:hint="@string/hint_due_date_date" + android:importantForAutofill="no" + android:inputType="date" + android:maxLines="1" + tools:text="01/07/2020" /> + + <EditText + android:id="@+id/dueDateTime" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_horizontal" + android:layout_weight="1" + android:enabled="true" + android:focusable="false" + android:hint="@string/hint_due_date_time" + android:importantForAutofill="no" + android:inputType="datetime" + android:maxLines="1" + android:minLines="0" + android:textAlignment="center" + tools:text="11:45" /> + + <ImageView + android:id="@+id/clearDueDate" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:contentDescription="@string/label_clear_due_date" + android:paddingStart="@dimen/spacer_1x" + android:paddingEnd="@dimen/spacer_1x" + app:srcCompat="@drawable/ic_close_circle_grey600" /> + </LinearLayout> + + <LinearLayout + android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_gravity="center_horizontal" - android:layout_weight="1" - android:enabled="true" - android:focusable="false" - android:hint="@string/hint_due_date_time" - android:importantForAutofill="no" - android:inputType="datetime" - android:maxLines="1" - android:minLines="0" - android:textAlignment="center" - tools:text="11:45" /> - - <ImageView - android:id="@+id/clearDueDate" - android:layout_width="wrap_content" - android:layout_height="match_parent" - android:contentDescription="@string/label_clear_due_date" - android:paddingStart="@dimen/spacer_1x" - android:paddingEnd="@dimen/spacer_1x" - app:srcCompat="@drawable/ic_close_circle_grey600" /> - </LinearLayout> - - <LinearLayout - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginTop="@dimen/spacer_2x" - android:orientation="horizontal"> - - <ImageView - android:layout_width="@dimen/icon_size_details" - android:layout_height="match_parent" - android:layout_marginEnd="@dimen/spacer_2x" - android:contentDescription="@null" - app:srcCompat="@drawable/ic_person_grey600_24dp" /> + android:layout_marginTop="@dimen/spacer_2x" + android:orientation="horizontal"> + + <ImageView + android:layout_width="@dimen/icon_size_details" + android:layout_height="match_parent" + android:layout_marginEnd="@dimen/spacer_2x" + android:contentDescription="@null" + app:srcCompat="@drawable/ic_person_grey600_24dp" /> + + <it.niedermann.nextcloud.deck.ui.view.ToggleAutoCompleteTextView + android:id="@+id/people" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:completionThreshold="1" + android:hint="@string/hint_assign_people" + android:inputType="text" /> + </LinearLayout> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/assignees" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginStart="40dp" + android:layout_marginTop="@dimen/spacer_1x" + app:layoutManager="androidx.recyclerview.widget.GridLayoutManager" + tools:listitem="@tools:sample/avatars" /> - <it.niedermann.nextcloud.deck.ui.view.ToggleAutoCompleteTextView - android:id="@+id/people" + <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" - android:completionThreshold="1" - android:hint="@string/hint_assign_people" - android:inputType="text" /> + android:layout_marginTop="@dimen/spacer_2x" + android:orientation="horizontal"> + + <ImageView + android:layout_width="@dimen/icon_size_details" + android:layout_height="wrap_content" + android:layout_marginTop="10dp" + android:layout_marginEnd="@dimen/spacer_2x" + android:contentDescription="@null" + app:srcCompat="@drawable/ic_baseline_subject_24" /> + + <com.yydcdut.markdown.MarkdownEditText + android:id="@+id/description" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="top" + android:hint="@string/label_description" + android:importantForAutofill="no" + android:inputType="textMultiLine|textCapSentences" + android:scrollbars="vertical" /> + </LinearLayout> </LinearLayout> - <LinearLayout - android:id="@+id/peopleList" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginStart="40dp" - android:orientation="horizontal" /> - - <LinearLayout + <TextView + android:id="@+id/projectsTitle" + style="?attr/textAppearanceOverline" android:layout_width="match_parent" android:layout_height="wrap_content" + android:layout_below="@id/details" + android:layout_marginStart="@dimen/icon_size_details" android:layout_marginTop="@dimen/spacer_2x" - android:orientation="horizontal"> - - <ImageView - android:layout_width="@dimen/icon_size_details" - android:layout_height="wrap_content" - android:layout_marginTop="10dp" - android:layout_marginEnd="@dimen/spacer_2x" - android:contentDescription="@null" - app:srcCompat="@drawable/ic_format_align_left_black_24dp" /> + android:paddingStart="@dimen/spacer_4x" + android:paddingEnd="@null" + android:text="@string/projects_title" /> - <com.yydcdut.markdown.MarkdownEditText - android:id="@+id/description" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:gravity="top" - android:hint="@string/label_description" - android:importantForAutofill="no" - android:inputType="textMultiLine|textCapSentences" - android:scrollbars="vertical" /> - </LinearLayout> - - <RelativeLayout + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/projects" android:layout_width="match_parent" - android:layout_height="wrap_content"> - - <androidx.fragment.app.FragmentContainerView - android:id="@+id/commentsFragment" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginTop="@dimen/spacer_2x" /> - - </RelativeLayout> - - </LinearLayout> + android:layout_height="wrap_content" + android:layout_below="@id/projectsTitle" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" + tools:listitem="@layout/item_project" /> + </RelativeLayout> </ScrollView> 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_assignee.xml b/app/src/main/res/layout/item_assignee.xml new file mode 100644 index 000000000..961bdea30 --- /dev/null +++ b/app/src/main/res/layout/item_assignee.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="utf-8"?> + +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/avatar" + android:layout_width="@dimen/avatar_size" + android:layout_height="@dimen/avatar_size" + android:layout_gravity="center" + android:scaleType="centerCrop" + android:background="?attr/selectableItemBackgroundBorderless" + app:srcCompat="@drawable/ic_person_grey600_24dp"/> + +</FrameLayout>
\ 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..e5e719bd9 --- /dev/null +++ b/app/src/main/res/layout/item_card_compact.xml @@ -0,0 +1,96 @@ +<?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:cardElevation="@dimen/spacer_1qx" + 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:paddingEnd="@dimen/spacer_1x" + 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..ad775fd1b 100644 --- a/app/src/main/res/layout/item_card.xml +++ b/app/src/main/res/layout/item_card_default.xml @@ -9,8 +9,9 @@ android:layout_marginTop="@dimen/spacer_1x" android:layout_marginEnd="@dimen/spacer_2x" android:layout_marginBottom="@dimen/spacer_1x" - app:cardBackgroundColor="@color/bg_card" - android:focusable="true"> + android:focusable="true" + app:cardElevation="@dimen/spacer_1qx" + app:cardBackgroundColor="@color/bg_card"> <LinearLayout android:layout_width="match_parent" @@ -32,8 +33,8 @@ android:layout_height="wrap_content" android:layout_marginTop="4sp" android:layout_weight="1" - android:textSize="18sp" 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" /> @@ -62,6 +63,7 @@ android:drawablePadding="@dimen/spacer_1hx" android:gravity="center" android:padding="@dimen/spacer_1hx" + android:paddingEnd="@dimen/spacer_1x" android:textColor="@color/fg_secondary" app:drawableStartCompat="@drawable/calendar_blank_grey600_24dp" tools:text="tomorrow" /> @@ -69,7 +71,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" @@ -100,7 +102,6 @@ android:drawablePadding="@dimen/spacer_1hx" android:gravity="center_vertical" android:padding="@dimen/spacer_1hx" - app:drawableLeftCompat="@drawable/ic_comment_white_24dp" app:drawableStartCompat="@drawable/ic_comment_white_24dp" app:drawableTint="@color/grey600" tools:text="2" /> @@ -111,8 +112,7 @@ android:layout_height="wrap_content" android:gravity="center_vertical" android:padding="@dimen/spacer_1hx" - app:drawableLeftCompat="@drawable/ic_check_grey600_24dp" - app:drawableStartCompat="@drawable/ic_check_grey600_24dp" + tools:drawableStartCompat="@drawable/ic_check_grey600_24dp" tools:text="1/2" /> <androidx.appcompat.widget.AppCompatTextView @@ -121,7 +121,6 @@ android:layout_height="wrap_content" android:gravity="center_vertical" android:padding="@dimen/spacer_1hx" - app:drawableLeftCompat="@drawable/ic_attach_file_grey600_24dp" app:drawableStartCompat="@drawable/ic_attach_file_grey600_24dp" tools:text="3" /> </LinearLayout> 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..a8db3d4e0 --- /dev/null +++ b/app/src/main/res/layout/item_card_default_only_title.xml @@ -0,0 +1,79 @@ +<?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:cardElevation="@dimen/spacer_1qx" + 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:paddingEnd="@dimen/spacer_1x" + 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/layout/item_prepare_create_account.xml b/app/src/main/res/layout/item_prepare_create_account.xml index f21c71326..9a76b898e 100644 --- a/app/src/main/res/layout/item_prepare_create_account.xml +++ b/app/src/main/res/layout/item_prepare_create_account.xml @@ -14,7 +14,7 @@ android:layout_marginEnd="@dimen/spacer_2x" android:contentDescription="@null" app:srcCompat="@drawable/ic_person_grey600_24dp" - tools:srcCompat="@tools:sample/avatars" /> + tools:src="@tools:sample/avatars" /> <LinearLayout @@ -37,8 +37,10 @@ android:layout_height="wrap_content" android:layout_gravity="center" android:layout_weight="1" + android:ellipsize="middle" + android:singleLine="true" android:textAppearance="?attr/textAppearanceListItemSecondary" - tools:text="https://example.com/" /> + tools:text="example.com" /> </LinearLayout> </LinearLayout>
\ No newline at end of file diff --git a/app/src/main/res/layout/item_prepare_create_stack.xml b/app/src/main/res/layout/item_prepare_create_stack.xml index f5a1f97b1..516941997 100644 --- a/app/src/main/res/layout/item_prepare_create_stack.xml +++ b/app/src/main/res/layout/item_prepare_create_stack.xml @@ -6,9 +6,9 @@ android:layout_height="wrap_content" android:ellipsize="middle" android:paddingStart="72dp" - android:paddingTop="@dimen/spacer_2x" + android:paddingTop="20dp" android:paddingEnd="@dimen/spacer_2x" - android:paddingBottom="@dimen/spacer_2x" + android:paddingBottom="20dp" android:singleLine="true" android:textAppearance="?attr/textAppearanceListItem" tools:text="@tools:sample/full_names" /> diff --git a/app/src/main/res/layout/item_project.xml b/app/src/main/res/layout/item_project.xml new file mode 100644 index 000000000..47ecf732d --- /dev/null +++ b/app/src/main/res/layout/item_project.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="?attr/selectableItemBackground" + android:gravity="center_vertical" + android:orientation="horizontal" + android:padding="@dimen/spacer_2x"> + + <androidx.appcompat.widget.AppCompatImageView + android:layout_width="@dimen/icon_size_details" + android:layout_height="@dimen/icon_size_details" + android:contentDescription="@null" + android:focusable="false" + android:scaleType="center" + android:layout_marginEnd="@dimen/spacer_2x" + app:srcCompat="@drawable/ic_projects_24" /> + + <LinearLayout + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:orientation="vertical"> + + <TextView + android:id="@+id/projectName" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:ellipsize="middle" + android:singleLine="true" + android:textAppearance="?attr/textAppearanceListItem" + tools:text="@tools:sample/full_names" /> + + <TextView + android:id="@+id/resourcesCount" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:ellipsize="middle" + android:singleLine="true" + android:textAppearance="?attr/textAppearanceListItemSecondary" + tools:text="4 resources" /> + </LinearLayout> +</LinearLayout> diff --git a/app/src/main/res/layout/item_project_resource.xml b/app/src/main/res/layout/item_project_resource.xml new file mode 100644 index 000000000..6288b2c85 --- /dev/null +++ b/app/src/main/res/layout/item_project_resource.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="?attr/selectableItemBackground" + android:gravity="center_vertical" + android:orientation="horizontal" + android:padding="@dimen/spacer_2x"> + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/image" + android:layout_width="@dimen/icon_size_details" + android:layout_height="@dimen/icon_size_details" + android:layout_marginEnd="@dimen/spacer_2x" + android:contentDescription="@null" + android:focusable="false" + android:scaleType="fitCenter" + app:srcCompat="@drawable/ic_projects_24" /> + + <LinearLayout + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:orientation="vertical"> + + <TextView + android:id="@+id/name" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:ellipsize="middle" + android:singleLine="true" + android:textAppearance="?attr/textAppearanceListItem" + tools:text="@tools:sample/full_names" /> + + <TextView + android:id="@+id/type" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:ellipsize="middle" + android:singleLine="true" + android:textAppearance="?attr/textAppearanceListItemSecondary" + tools:text="4 resources" /> + </LinearLayout> +</LinearLayout> diff --git a/app/src/main/res/layout/item_tip.xml b/app/src/main/res/layout/item_tip.xml index 2177d013c..fc2d3587a 100644 --- a/app/src/main/res/layout/item_tip.xml +++ b/app/src/main/res/layout/item_tip.xml @@ -15,7 +15,6 @@ android:drawablePadding="@dimen/spacer_2x" android:gravity="start|center" android:textAppearance="?attr/textAppearanceListItem" - app:drawableLeftCompat="@drawable/ic_lightbulb_outline_grey600_24dp" app:drawableStartCompat="@drawable/ic_lightbulb_outline_grey600_24dp" tools:maxLength="200" tools:text="@tools:sample/lorem/random" /> diff --git a/app/src/main/res/layout/widget_stack.xml b/app/src/main/res/layout/widget_stack.xml new file mode 100644 index 000000000..d920bff87 --- /dev/null +++ b/app/src/main/res/layout/widget_stack.xml @@ -0,0 +1,64 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@drawable/widget_single_card_background" + android:orientation="vertical"> + + <!-- Widget header --> + <RelativeLayout + android:id="@+id/widget_stack_header_rl" + android:layout_width="match_parent" + android:layout_height="@dimen/widget_stack_header_height" + android:padding="@dimen/widget_stack_card_padding"> + + <ImageView + android:id="@+id/widget_stack_header_icon" + android:layout_width="@dimen/widget_stack_icon_width" + android:layout_height="match_parent" + android:src="@drawable/circle_grey600_8dp" + android:layout_alignParentStart="true" + android:layout_alignParentBottom="true" + android:paddingStart="@dimen/widget_stack_card_padding" + android:paddingEnd="@dimen/widget_stack_card_padding" + android:contentDescription="@string/widget_stack_header_icon" /> + + <TextView + android:id="@+id/widget_stack_title_tv" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:textAppearance="?attr/textAppearanceBody1" + android:textSize="18sp" + android:layout_toEndOf="@id/widget_stack_header_icon" + android:gravity="center_vertical" + android:paddingStart="@dimen/widget_stack_card_padding" + android:paddingEnd="@dimen/widget_stack_card_padding" + android:textColor="@color/widget_foreground" + tools:text="@string/app_name"/> + + </RelativeLayout> + <!-- End header --> + + <ListView + android:id="@+id/stack_widget_lv" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:divider="@android:color/transparent" + android:dividerHeight="@dimen/spacer_1x" + android:padding="@dimen/spacer_1x" + tools:listitem="@layout/widget_stack_entry" /> + + <ImageView + android:id="@+id/widget_stack_placeholder_iv" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:gravity="center" + android:text="@string/app_name_short" + android:textColor="@color/fg_secondary" + app:srcCompat="@drawable/ic_local_movies_grey600_24dp" + tools:visibility="gone" + android:contentDescription="@string/widget_stack_placeholder_icon" /> + +</LinearLayout> diff --git a/app/src/main/res/layout/widget_stack_entry.xml b/app/src/main/res/layout/widget_stack_entry.xml new file mode 100644 index 000000000..f67a53e61 --- /dev/null +++ b/app/src/main/res/layout/widget_stack_entry.xml @@ -0,0 +1,24 @@ +<?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:id="@+id/widget_stack_entry" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:padding="@dimen/widget_stack_card_padding" + android:background="@color/primary"> + + <TextView + android:id="@+id/widget_entry_content_tv" + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_gravity="center_vertical" + android:layout_weight="1" + android:paddingStart="@dimen/widget_stack_card_padding" + android:paddingEnd="@dimen/widget_stack_card_padding" + android:textAppearance="?attr/textAppearanceListItem" + android:textSize="14sp" + android:textColor="@color/widget_foreground" + tools:maxLength="60" + tools:text="@tools:sample/lorem/random" /> +</LinearLayout>
\ 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-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 1e3f1fb8b..abb3667fe 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -38,6 +38,7 @@ <string name="simple_disabled">Desactivat</string> <string name="simple_copied">Copiat</string> <string name="simple_archive">Arxiu</string> + <string name="simple_unassigned">Sense assignar</string> <string name="edit_board">Edita el tauler</string> <string name="archive_board">Arxiva el tauler</string> @@ -70,9 +71,6 @@ <string name="about_credits_tab_title">Crèdits</string> <string name="about_contribution_tab_title">Contribució</string> <string name="about_license_tab_title">Llicència</string> - - <string name="copied_to_clipboard">S\'ha copiat al porta-retalls</string> - <string name="seconds_ago">fa uns segons</string> <string name="edit">Edita</string> <string name="label_labels">Selecciona etiquetes</string> @@ -129,6 +127,7 @@ <string name="delete_board_message">Això suprimirà permanentment aquest tauler, incloent-hi totes les llistes i targetes.</string> <string name="settings_theme_title">Tema fosc</string> <string name="settings_branding_title">Marcatge</string> + <string name="settings_compact_title">Mode compacte</string> <string name="settings_background_sync">Sincronització en segon pla</string> <string name="pref_value_wifi_and_mobile">Sincronitza per Wi-Fi i dades mòbils</string> <string name="pref_value_wifi_only">Sincronitza només per Wi-Fi</string> @@ -156,7 +155,6 @@ <string name="maintenance_mode_explanation">El servidor %1$s és actualment en mode de manteniment. Si us plau, contacteu el vostre administrador o proveu de nou més tard.</string> <string name="share_add_to_card">Afegeix a la targeta</string> <string name="share_success">S\'ha afegit amb èxit %1$s a %2$s</string> - <string name="could_not_copy_to_clipboard">No s\'ha pogut copiar al porta-retalls</string> <string name="add_comment">Afegeix un comentari</string> <string name="card_edit_comments">Comentaris</string> <string name="no_comments_yet">Encara no hi ha comentaris</string> @@ -216,7 +214,8 @@ <string name="error_dialog_title">Oh no! - I ara què? 🙁</string> <string name="error_dialog_tip_token_mismatch_retry">Si us plau, mireu de forçar el tancament de l\'aplicació i reiniciar-la de nou. Hi deu haver hagut una connexió incorrecta a l\'aplicació Nextcloud.</string> - <string name="error_dialog_tip_token_mismatch_clear_storage">Si el problema persisteix, mireu de netejar l\'emmagatzematge de totes dues aplicacions: Nextcloud i Nextcloud Deck per tal de resoldre el problema.</string> + <string name="error_dialog_tip_clear_storage_might_help">Si el problema persisteix, mireu de netejar l\'emmagatzematge de totes dues aplicacions: Nextcloud i Nextcloud Deck per tal de resoldre el problema.</string> + <string name="error_dialog_tip_database_upgrade_failed">Ha fallat l\'actualització de la base de dades. Informeu del problema i esborreu l\'emmagatzematge per utilitzar l\'aplicació normalment.</string> <string name="error_dialog_tip_clear_storage">Podeu netejar l\'emmagatzematge obrint la informació de l\'aplicació i seleccionant Emmagatzematge → Neteja l\'emmagatzematge.</string> <string name="error_dialog_tip_files_outdated">La vostra aplicació Nextcloud sembla que està obsoleta. Si us plau, visiteu la Play Store o l\'F-Droid per tal d\'aconseguir la darrera versió.</string> <string name="error_dialog_tip_files_force_stop">Sembla que alguna cosa està malament a la vostra aplicació Nextcloud. Si us plau, intenteu forçar l\'aturada tant de l\'aplicació Nextcloud com de l\'aplicació Nextcloud Deck.</string> @@ -230,7 +229,11 @@ <string name="error_dialog_we_need_info">Ens cal la següent informació tècnica per poder-vos ajudar:</string> <string name="error_dialog_redirect">El vostre servidor ha respost amb un codi d\'estat HTTP 302, que vol dir que no teniu instal·lada l\'aplicació Deck al vostre servidor o que alguna cosa està mal configurada. Això pot estar causat per una sobreescriptura a .htaccess-file o per aplicacions de Nextcloud com OID Client.</string> <string name="error_dialog_version_not_parsable">No hem pogut determinar la versió de l\'aplicació Deck de la part del servidor. Si us plau, assegureu-vos que està instal·lada i habilitada.</string> + <string name="error_dialog_account_might_not_be_authorized">És possible que el compte de la vostra aplicació Nextcloud ja no estigui autoritzat.</string> + <string name="error_dialog_user_not_found_in_database">L’usuari actual no coincideix amb l’usuari que tenim a la nostra base de dades. Si feu servir LDAP a la vostra instància de Nextcloud, és possible que l\'aplicació Nextcloud hagi emmagatzemat un ID d\'usuari antic.</string> <string name="error_dialog_capabilities_not_parsable">No podem recuperar les capacitats del vostre servidor. Si us plau, assegureu-vos que el servidor funciona bé i que altres aplicacions de client poden accedir al Nextcloud.</string> + <string name="error_dialog_attachment_upload_failed">No s\'ha pogut carregar un fitxer adjunt. Proveu de compartir-ho d’una altra manera i informeu-nos d’aquest error.</string> + <string name="error_dialog_tip_disable_battery_optimizations">Desactiveu totes les optimitzacions de bateries de Nextcloud i l\'aplicació Deck.</string> <string name="error_action_open_deck_info">Obre la informació de l\'aplicació</string> <string name="error_action_open_network">Arranjaments de xarxa</string> <string name="error_action_server_logs">Bitàcoles del servidor</string> @@ -240,9 +243,43 @@ <string name="info_box_version_not_supported">La versió del servidor %1$s no està suportada, si us plau, actualitzeu a %2$s.</string> <string name="share_link">Enllaç de compartició</string> <string name="archive_cards">Arxiva les targetes</string> + <string name="manage_accounts">Gestiona els comptes</string> <string name="manage_list">Gestiona la llista</string> <string name="simple_reply">Respon</string> + <string name="error_while_uploading_attachment">S\'ha produït un error en penjar el fitxer adjunt: %1$s</string> + <string name="append_text_to_description">Apunteu a la descripció</string> + <string name="add_text_as_comment">Afegir com a comentari</string> + <string name="progress_count">%1$d de %2$d</string> + <plurals name="progress_error_count"> + <item quantity="one">%1$d error durant la càrrega</item> + <item quantity="other">%1$d errors durant la càrrega</item> + </plurals> <string name="simple_report">Informar</string> <string name="error_action_open_battery_settings">Paràmetres de la bateria</string> + <string name="move_warning">Ni es poden transferir comentaris ni fitxers adjunts quan es mogui la targeta a un altre tauler.</string> + <string name="clone_board">Clonar tauler</string> + <string name="cloning_board">Clonant %1$s ...</string> + <string name="successfully_cloned_board">S\'ha clonat amb èxit %1$s</string> + <string name="attachment_does_not_yet_exist">Encara no existeix el fitxer adjunt al Deck</string> + <string name="card_does_not_yet_exist">La targeta encara no existeix al Deck</string> + + <string name="widget_stack_title">Llista</string> + <string name="widget_stack_header_icon">Icona de capçalera de widget</string> + <string name="widget_stack_placeholder_icon">Icona de marcador de lloc de widget</string> + <string name="select_stack">Seleccionar llista</string> + <string name="project_type_deck_board">Tauler de Deck</string> + <string name="project_type_deck_card">Targeta de Deck</string> + <string name="project_type_file">Fitxer</string> + <string name="projects_title">Projectes</string> + <plurals name="resources_count"> + <item quantity="one">%1$d recursos</item> + <item quantity="other">%1$d recursos</item> + </plurals> + <string name="no_assigned_label">Sense etiqueta assignada</string> + <string name="single_card">Targeta única</string> + <string name="project_type_room">Sala de xerrades</string> + <string name="simple_move">Mou</string> + <string name="cannot_upload_files_without_permission">No es poden pujar fitxers sense permís</string> + <string name="simple_unassign">No ho assignis</string> </resources> diff --git a/app/src/main/res/values-cs-rCZ/strings.xml b/app/src/main/res/values-cs-rCZ/strings.xml index 3bc6b422e..33354922d 100644 --- a/app/src/main/res/values-cs-rCZ/strings.xml +++ b/app/src/main/res/values-cs-rCZ/strings.xml @@ -38,6 +38,7 @@ <string name="simple_disabled">vypnuto</string> <string name="simple_copied">Zkopírováno</string> <string name="simple_archive">Archiv</string> + <string name="simple_unassigned">Nepřiřazeno</string> <string name="edit_board">Upravit tabuli</string> <string name="archive_board">Archivovat tabuli</string> @@ -70,9 +71,6 @@ <string name="about_credits_tab_title">Zásluhy</string> <string name="about_contribution_tab_title">Zapojení se</string> <string name="about_license_tab_title">Licence</string> - - <string name="copied_to_clipboard">Zkopírováno do schránky</string> - <string name="seconds_ago">před několika sekundami</string> <string name="edit">Upravit</string> <string name="label_labels">Vybrat štítky</string> @@ -113,6 +111,7 @@ <string name="no_lists_yet">Zatím žádné seznamy</string> <string name="do_you_want_to_save_your_changes">Chcete vámi provedené změny uložit?</string> <string name="do_you_want_to_archive_all_cards_of_the_list">Chcete archivovat všechny karty %1$s?</string> + <string name="do_you_want_to_archive_all_cards_of_the_filtered_list">Chcete archivovat všechny filtrované karty %1$s?</string> <plurals name="do_you_want_to_delete_the_current_list"> <item quantity="one">Toto nadobro vymaže %1$d kartu z tohoto seznamu.</item> <item quantity="few">Toto nadobro vymaže obě %1$d karty z tohoto seznamu.</item> @@ -133,6 +132,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> @@ -160,7 +160,6 @@ <string name="maintenance_mode_explanation">Na serveru %1$s v tuto chvíli probíhá údržba. Obraťte se na jeho správce nebo to zkuste později znovu.</string> <string name="share_add_to_card">Přidat do karty</string> <string name="share_success">%1$s úspěšně přidáno do %2$s</string> - <string name="could_not_copy_to_clipboard">Nedaří se zkopírovat do schránky</string> <string name="add_comment">Přidat komentář</string> <string name="card_edit_comments">Komentáře</string> <string name="no_comments_yet">Zatím žádné komentáře</string> @@ -220,7 +219,7 @@ <string name="error_dialog_title">Ó ne– co teď? 🙁</string> <string name="error_dialog_tip_token_mismatch_retry">Prosím pokuste se vynutit ukončení aplikace a spusťte ji znovu. Možná se něco nepodařilo při spojení s aplikací Nextcloud.</string> - <string name="error_dialog_tip_token_mismatch_clear_storage">Pokud problém přetrvává, zkuste problém vyřešit vyčištěním úložišť obou aplikací: Nextcloud a Nextcloud Deck.</string> + <string name="error_dialog_tip_clear_storage_might_help">Pokud problém přetrvává, zkuste problém vyřešit vyčištěním úložišť obou aplikací: Nextcloud a Nextcloud Deck.</string> <string name="error_dialog_tip_database_upgrade_failed">Přechod na novější verzi databáze se nezdařil. Prosíme nahlaste tento problém vývojářům a aby bylo možné aplikaci normálně používat, vyčistěte úložiště.</string> <string name="error_dialog_tip_clear_storage">Úložiště můžete vyčistit otevřením informací o aplikací a vybráním Úložiště → Vyčistit úložiště.</string> <string name="error_dialog_tip_files_outdated">Zdá se, že aplikace Nextcloud, nainstalovaná na vašem zařízení, je staré verze. Přejděte prosím do Play Store nebo F-Droid a nainstalujte si nejnovější verzi.</string> @@ -235,6 +234,8 @@ <string name="error_dialog_we_need_info">Abychom vám mohli pomoci, potřebujeme následující technické údaje:</string> <string name="error_dialog_redirect">Vámi využívaný server odpověděl HTTP stavovým kódem 302, což ukazuje na to, že na serveru buď není nainstalovaná aplikace Deck, nebo je něco nesprávně nastavené. Toto může být způsobeno uživatelsky určenými přepsáními v souboru .htaccess nebo Nextcloud aplikacemi jako je OID klient.</string> <string name="error_dialog_version_not_parsable">Nepodařilo se zjistit verze aplikace Deck na straně serveru. Zajistěte, aby byla nainstalovaná a zapnutá.</string> + <string name="error_dialog_account_might_not_be_authorized">Může být, že účet vaší Nextcloud aplikace už nemá potřebná pověření.</string> + <string name="error_dialog_user_not_found_in_database">Stávající uživatel se neshoduje s uživatelem, který je v databázi. Pokud na své instanci Nextcloud používáte LDAP, je možné, že Nextcloud aplikace má uložený starý identifikátor uživatele.</string> <string name="error_dialog_capabilities_not_parsable">Nepodařilo se zjistit schopnosti serveru, který využíváte. Ověřte, že server v pořádku běží a ostatní klientské aplikace mohou k Nextcloud přistupovat.</string> <string name="error_dialog_attachment_upload_failed">Přílohu se nepodařilo nahrát. Zkuste ji nasdílet jiným způsobem a dejte nám o této chybě vědět.</string> <string name="error_dialog_tip_disable_battery_optimizations">V nastavení systému prosím pro aplikace Nextcloud a Deck vypněte veškeré optimalizace spotřeby energie z akumulátoru.</string> @@ -247,6 +248,7 @@ <string name="info_box_version_not_supported">Verze serveru %1$s není podporována, přejděte na verzi %2$s</string> <string name="share_link">Odkaz na sdílení</string> <string name="archive_cards">Archivovat karty</string> + <string name="manage_accounts">Spravovat účty</string> <string name="manage_list">Spravovat sloupec</string> <string name="simple_reply">Odpovědět</string> @@ -255,11 +257,41 @@ <string name="add_text_as_comment">Přidat jako komentář</string> <string name="progress_count">%1$d z %2$d</string> <plurals name="progress_error_count"> - <item quantity="one">%1$d chyba při nahrávání.</item> - <item quantity="few">%1$d chyby při nahrávání.</item> - <item quantity="many">%1$d chyb při nahrávání.</item> - <item quantity="other">%1$d chyby při nahrávání.</item> + <item quantity="one">%1$d chyba při nahrávání</item> + <item quantity="few">%1$d chyby při nahrávání</item> + <item quantity="many">%1$d chyb při nahrávání</item> + <item quantity="other">%1$d chyby při nahrávání</item> </plurals> <string name="simple_report">Hlášení</string> <string name="error_action_open_battery_settings">Nastavení správy napájení</string> + <string name="move_warning">Při přesunutí karty na jinou tabuli není možné přenést také komentáře či přílohy.</string> + <string name="clone_board">Klonovat tabuli</string> + <string name="cloning_board">Klonování %1$s…</string> + <string name="successfully_cloned_board">%1$s úspěšně naklonováno</string> + <string name="attachment_does_not_yet_exist">Příloha ještě neexistuje v Deck</string> + <string name="card_does_not_yet_exist">Karta ještě neexistuje v Deck</string> + + <string name="widget_stack_title">Seznam</string> + <string name="widget_stack_header_icon">Ikona hlavičky ovládacího prvku</string> + <string name="widget_stack_placeholder_icon">Ikona výplně ovládacího prvku</string> + <string name="select_stack">Vybrat seznam</string> + <string name="project_type_deck_board">Tabule aplikace Deck</string> + <string name="project_type_deck_card">Karta aplikace Deck</string> + <string name="project_type_file">Soubor</string> + <string name="projects_title">Projekty</string> + <plurals name="resources_count"> + <item quantity="one">%1$d prostředek</item> + <item quantity="few">%1$d prostředky</item> + <item quantity="many">%1$d prostředků</item> + <item quantity="other">%1$d prostředky</item> + </plurals> + <string name="no_assigned_label">Nepřiřazena značka</string> + <string name="single_card">Jedna karta</string> + <string name="project_type_room">Diskuzní místnost</string> + <string name="simple_move">Přesunout</string> + <string name="cannot_upload_files_without_permission">Není možné nahrávat soubory bez oprávnění</string> + <string name="clone_cards">Klonovat karty</string> + <string name="simple_clone">Klonovat</string> + <string name="user_avatar">Zástupný obrázek uživatele</string> + <string name="simple_unassign">Zrušit přirazení</string> </resources> diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 8ac1e8373..efd98ec81 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -37,7 +37,6 @@ <string name="simple_disabled">slået fra</string> <string name="simple_copied">opieret</string> <string name="simple_archive">Arkivér</string> - <string name="edit_board">Rediger liste</string> <string name="archive_board">Arkivér liste</string> <string name="delete_board">Slet liste</string> diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 69f7a1886..7d6ef9abe 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -24,7 +24,7 @@ <string name="simple_switch">Wechseln</string> <string name="simple_filter">Filter</string> <string name="simple_overdue">Überfällig</string> - <string name="simple_clear">Löschen</string> + <string name="simple_clear">Leeren</string> <string name="simple_discard">Verwerfen</string> <string name="simple_update">Aktualisieren</string> <string name="simple_delete">Löschen</string> @@ -38,6 +38,7 @@ <string name="simple_disabled">Deaktiviert</string> <string name="simple_copied">Kopiert</string> <string name="simple_archive">Archivieren</string> + <string name="simple_unassigned">Nicht zugewiesen</string> <string name="edit_board">Board bearbeiten</string> <string name="archive_board">Board archivieren</string> @@ -70,9 +71,6 @@ <string name="about_credits_tab_title">Credits</string> <string name="about_contribution_tab_title">Beitrag</string> <string name="about_license_tab_title">Lizenz</string> - - <string name="copied_to_clipboard">In die Zwischenablage kopiert</string> - <string name="seconds_ago">Gerade eben</string> <string name="edit">Bearbeiten</string> <string name="label_labels">Schlagworte auswählen</string> @@ -96,7 +94,7 @@ <string name="action_card_delete">Karte löschen</string> <string name="add_board">Board hinzufügen</string> - <string name="label_clear_due_date">Fälligkeitsdatum löschen</string> + <string name="label_clear_due_date">Fälligkeitsdatum entfernen</string> <string name="label_add">%1$s hinzufügen</string> <string name="card_edit_details">Details</string> @@ -113,6 +111,7 @@ <string name="no_lists_yet">Noch keine Listen vorhanden</string> <string name="do_you_want_to_save_your_changes">Möchten Sie Ihre Änderungen speichern?</string> <string name="do_you_want_to_archive_all_cards_of_the_list">Möchten Sie alle Karten von %1$s archivieren?</string> + <string name="do_you_want_to_archive_all_cards_of_the_filtered_list">Möchten Sie alle gefilterten Karten von %1$s archivieren?</string> <plurals name="do_you_want_to_delete_the_current_list"> <item quantity="one">Dies wird %1$d Karte permanent aus dieser Liste löschen.</item> <item quantity="other">Dies wird %1$d Karten permanent aus dieser Liste löschen.</item> @@ -129,6 +128,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> @@ -156,7 +156,6 @@ <string name="maintenance_mode_explanation">Der Server %1$s ist momentan im Wartungsmodus. Bitte nehmen Sie Kontakt mit Ihrem Administrator auf oder versuchen Sie es später nochmal!</string> <string name="share_add_to_card">Zur Karte hinzufügen</string> <string name="share_success">%1$s erfolgreich zu %2$s hinzugefügt</string> - <string name="could_not_copy_to_clipboard">Konnte nicht in die Zwischenablage kopieren</string> <string name="add_comment">Kommentar hinzufügen</string> <string name="card_edit_comments">Kommentare</string> <string name="no_comments_yet">Noch keine Kommentare vorhanden</string> @@ -216,12 +215,12 @@ <string name="error_dialog_title">Oh nein - Was jetzt? 🙁</string> <string name="error_dialog_tip_token_mismatch_retry">Bitte versuchen Sie, das Schließen der App zu erzwingen und erneut zu starten. Möglicherweise bestand eine gestörte Verbindung zur Nextcloud-App.</string> - <string name="error_dialog_tip_token_mismatch_clear_storage">Falls dieser Fehler weiterhin besteht können Sie versuchen den Speicherinhalt der beiden Apps Nextcloud und Nextcloud Deck zu löschen um das Problem zu beheben.</string> - <string name="error_dialog_tip_database_upgrade_failed">Die Aktualisierung der Datenbank ist fehlgeschlagen. Bitte melden Sie das Problem und löschen Sie den Speicher, um die App normal zu verwenden.</string> - <string name="error_dialog_tip_clear_storage">Sie können den Speicherinhalt löschen indem Sie die App öffnen und Speicher → Speicherinhalt löschen auswählen.</string> + <string name="error_dialog_tip_clear_storage_might_help">Falls dieser Fehler weiterhin besteht können Sie versuchen den Speicherinhalt der beiden Apps Nextcloud und Nextcloud Deck zu leeren um das Problem zu beheben.</string> + <string name="error_dialog_tip_database_upgrade_failed">Die Aktualisierung der Datenbank ist fehlgeschlagen. Bitte melden Sie das Problem und leeren Sie den Speicher, um die App normal zu verwenden.</string> + <string name="error_dialog_tip_clear_storage">Sie können den Speicherinhalt leeren indem Sie die App öffnen und Speicher → Speicherinhalt löschen auswählen.</string> <string name="error_dialog_tip_files_outdated">Sie scheinen eine alte Version der Nextcloud-App installiert zu haben. Bitte besuchen Sie den Play Store oder F-Droid, um die neueste Version zu installieren.</string> <string name="error_dialog_tip_files_force_stop">Etwas scheint bei der Nextcloud App nicht zu funktionieren. Bitte versuchen Sie sowohl die Nextcloud App als auch die Nextcloud Deck App zu stoppen.</string> - <string name="error_dialog_tip_files_delete_storage">Wenn das erzwungene Stoppen nicht hilft, können Sie versuchen, den Speicher beider Apps zu löschen.</string> + <string name="error_dialog_tip_files_delete_storage">Wenn das erzwungene Stoppen nicht hilft, können Sie versuchen, den Speicher beider Apps zu leeren .</string> <string name="error_dialog_timeout_instance">Der Server reagierte nicht in der üblichen Zeit. Bitte stellen Sie sicher, dass Ihre Instanz einwandfrei läuft.</string> <string name="error_dialog_timeout_toggle">Überprüfen Sie Ihre Netzwerkverbindung. Manchmal kann es hilfreich sein, die mobilen Daten oder das WLAN aus- und wieder einzuschalten.</string> <string name="error_dialog_check_server">Die Antwort des Servers war fehlerhaft. Bitte überprüfen Sie ob die Deck App über die Weboberfläche erreichbar ist.</string> @@ -231,6 +230,8 @@ <string name="error_dialog_we_need_info">Wir benötigen die folgenden technischen Informationen, um Ihnen helfen zu können:</string> <string name="error_dialog_redirect">Ihr Server hat mit einem HTTP 302-Statuscode geantwortet, was bedeutet, dass Sie die Deck-App nicht auf Ihrem Server installiert haben oder etwas falsch konfiguriert ist. Dies kann durch benutzerdefinierte Überschreibungen in einer .htaccess-Datei oder durch Nextcloud-Apps wie OID-Client verursacht werden.</string> <string name="error_dialog_version_not_parsable">Version der auf dem Server installierten Deck-App konnte nicht ermittelt werden. Bitte sicherstellen, dass die Deck-App intstalliert und aktiviert ist.</string> + <string name="error_dialog_account_might_not_be_authorized">Das Konto Ihrer Nextcloud-App ist möglicherweise nicht mehr autorisiert.</string> + <string name="error_dialog_user_not_found_in_database">Der aktuelle Benutzer stimmt nicht mit dem Benutzer überein, den wir in unserer Datenbank haben. Wenn Sie LDAP in Ihrer Nextcloud-Instanz verwenden, hat Ihre Nextcloud-App möglicherweise eine alte Benutzer-ID gespeichert.</string> <string name="error_dialog_capabilities_not_parsable">Funktionsumfang Ihres Serves konnte nicht nicht abgerufen werden. Stellen Sie sicher, dass Ihr Server ordnungsgemäß funktioniert und Client-Apps auf Nextcloud zugreifen können.</string> <string name="error_dialog_attachment_upload_failed">Ein Anhang konnte nicht hochgeladen werden. Bitte versuchen Sie es auf andere Weise zu teilen und teilen Sie uns diesen Fehler mit.</string> <string name="error_dialog_tip_disable_battery_optimizations">Bitte alle Batterieoptimierungen für Nextcloud und die Deck-App deaktivieren.</string> @@ -243,6 +244,7 @@ <string name="info_box_version_not_supported">Serverversion %1$s wird nicht unterstützt. Bitte aktualisiere auf %2$s.</string> <string name="share_link">Link teilen</string> <string name="archive_cards">Karten archivieren</string> + <string name="manage_accounts">Konten verwalten</string> <string name="manage_list">Liste verwalten</string> <string name="simple_reply">Antworten</string> @@ -256,4 +258,32 @@ </plurals> <string name="simple_report">Melden</string> <string name="error_action_open_battery_settings">Batterie-Einstellungen</string> + <string name="move_warning">Beim Verschieben einer Karte auf ein anderes Board werden weder Kommentare noch Anhänge transferriert.</string> + <string name="clone_board">Board klonen</string> + <string name="cloning_board">Klone %1$s…</string> + <string name="successfully_cloned_board">%1$s erfolgreich geklont</string> + <string name="attachment_does_not_yet_exist">Anhang existiert bislang nicht in Deck</string> + <string name="card_does_not_yet_exist">Karte existiert bislang nicht in Deck</string> + + <string name="widget_stack_title">Liste</string> + <string name="widget_stack_header_icon">Widget-Kopf-Icon</string> + <string name="widget_stack_placeholder_icon">Widget-Platzhalter-Symbol</string> + <string name="select_stack">Liste auswählen</string> + <string name="project_type_deck_board">Deck-Board</string> + <string name="project_type_deck_card">Deck-Karte</string> + <string name="project_type_file">Datei</string> + <string name="projects_title">Projekte</string> + <plurals name="resources_count"> + <item quantity="one">%1$d Ressource</item> + <item quantity="other">%1$d Ressourcen</item> + </plurals> + <string name="no_assigned_label">Keine Schlagwort zugewiesen</string> + <string name="single_card">Einzelne Karte</string> + <string name="project_type_room">Talk-Raum</string> + <string name="simple_move">Verschieben</string> + <string name="cannot_upload_files_without_permission">Dateien können nicht ohne Berechtigung hochgeladen werden</string> + <string name="clone_cards">Karten klonen</string> + <string name="simple_clone">Klonen</string> + <string name="user_avatar">Benutzer-Avatar</string> + <string name="simple_unassign">Zuordnung aufheben</string> </resources> diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 5ef209258..5766f15f2 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -38,6 +38,7 @@ <string name="simple_disabled">απενεργοποιημένο</string> <string name="simple_copied">Αντιγράφτηκε</string> <string name="simple_archive">Αρχειοθέτηση</string> + <string name="simple_unassigned">Χωρίς ανάθεση</string> <string name="edit_board">Επεξεργασία πίνακα</string> <string name="archive_board">Αρχειοθέτηση πίνακα</string> @@ -70,9 +71,6 @@ <string name="about_credits_tab_title">Μνεία</string> <string name="about_contribution_tab_title">Συνεισφορά</string> <string name="about_license_tab_title"> Άδεια χρήσης </string> - - <string name="copied_to_clipboard">Αντιγράφηκε στο πρόχειρο</string> - <string name="seconds_ago"> δευτερόλεπτα πριν </string> <string name="edit">Επεξεργασία</string> <string name="label_labels">Επιλογή Ετικετών</string> @@ -129,6 +127,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> @@ -156,7 +155,6 @@ <string name="maintenance_mode_explanation">Ο διακομιστής %1$s είναι σε κατάσταση συντήρησης. Επικοινωνήστε με τον διαχειριστή ή δοκιμάστε αργότερα.</string> <string name="share_add_to_card">Προσθήκη στην κάρτα</string> <string name="share_success">Επιτυχής προσθήκη %1$s από %2$s</string> - <string name="could_not_copy_to_clipboard">Δεν μπορεί να αντιγραφή στο πρόχειρο</string> <string name="add_comment">Προσθήκη σχολίου</string> <string name="card_edit_comments">Σχόλια</string> <string name="no_comments_yet">Δεν υπάρχουν ακόμα σχόλια</string> @@ -216,7 +214,8 @@ <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_clear_storage_might_help">Εάν το πρόβλημα παραμένει, προσπαθήστε να εκκαθαρίσετε τον χώρο αποθήκευσης και των δύο εφαρμογών: του 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> @@ -230,8 +229,11 @@ <string name="error_dialog_we_need_info">Χρειαζόμαστε τις ακόλουθες τεχνικές πληροφορίες για να σας βοηθήσουμε:</string> <string name="error_dialog_redirect">Ο διακομιστής απάντησε με κωδικό κατάστασης HTTP 302, που σημαίνει, πως δεν έχετε εγκατεστημένη την εφαρμογή Deck στον διακομιστή σας ή υπάρχει λάθος ρύθμιση. Αυτό μπορεί να προκλήθηκε από λάθος καταχώρηση στο αρχείο .htaccess-file ή σε εφαρμογές του Nextcloud όπως την OID Client.</string> <string name="error_dialog_version_not_parsable">Δεν μπορέσαμε να προσδιορίσουμε την έκδοση της εφαρμογής Deck του διακομιστή σας. Βεβαιωθείτε ότι είναι εγκατεστημένο και ενεργοποιημένο.</string> + <string name="error_dialog_account_might_not_be_authorized">Ο λογαριασμός στην εφαρμογή σας Nextcloud, ίσως δεν έχει πλέον πιστοποίηση.</string> + <string name="error_dialog_user_not_found_in_database">Ο τρέχων χρήστης δεν είναι ίδιος με αυτόν που έχουμε καταχωρημένο στη βάση δεδομένων μας. Εάν χρησιμοποιείτε την λειτουρία LDAP στον διακομιστή σας Nextcloud, η εφαρμογή σας ενδέχεται να έχει αποθηκευμένο ένα παλιό αναγνωριστικό χρήστη.</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> @@ -241,6 +243,7 @@ <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> @@ -253,4 +256,30 @@ <item quantity="other">%1$d σφάλματα κατά την μεταφόρτωση</item> </plurals> <string name="simple_report">Αναφορά</string> - </resources> + <string name="error_action_open_battery_settings">Ρυθμίσεις μπαταρίας</string> + <string name="move_warning">Δεν μπορούν να μεταφερθούν τα σχόλια, ούτε τα συνημμένα κατά τη μετακίνηση της κάρτας σε άλλο πίνακα.</string> + <string name="clone_board">Κλωνοποίηση πίνακα</string> + <string name="cloning_board">Κλωνοποίηση %1$s…</string> + <string name="successfully_cloned_board">Επιτυχής κλωνοποίηση %1$s</string> + <string name="attachment_does_not_yet_exist">Δεν υπάρχει ακόμα το συνημμένο στο Deck</string> + <string name="card_does_not_yet_exist">Η κάρτα δεν υπάρχει ακόμα στο Deck</string> + + <string name="widget_stack_title">Λίστα</string> + <string name="widget_stack_header_icon">Εικονίδιο επικεφαλίδας του γραφικού στοιχείου</string> + <string name="widget_stack_placeholder_icon">Εικονίδιο θέσης κράτησης του γραφικού στοιχείου</string> + <string name="select_stack">Επιλέξτε λίστα</string> + <string name="project_type_deck_board">Πίνακας του Deck</string> + <string name="project_type_deck_card">Κάρτα του Deck</string> + <string name="project_type_file">Αρχείο</string> + <string name="projects_title">Projects</string> + <plurals name="resources_count"> + <item quantity="one">%1$d πόρος</item> + <item quantity="other">%1$d πόροι</item> + </plurals> + <string name="no_assigned_label">Δεν καταχωρήθηκε ετικέτα</string> + <string name="single_card">Απλή καρτέλα</string> + <string name="project_type_room">Δωμάτιο του Talk </string> + <string name="simple_move">Μετακίνηση</string> + <string name="cannot_upload_files_without_permission">Δεν μπορείτε να ανεβάσετε αρχεία χωρίς άδεια</string> + <string name="simple_unassign">Αποδέσμευση</string> +</resources> diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index cf92736ed..0287028ae 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -38,6 +38,7 @@ <string name="simple_disabled">deshabilitado</string> <string name="simple_copied">Copiado</string> <string name="simple_archive">Archivar</string> + <string name="simple_unassigned">No asignado</string> <string name="edit_board">Editar tablero</string> <string name="archive_board">Archivar tablero</string> @@ -70,9 +71,6 @@ <string name="about_credits_tab_title">Créditos</string> <string name="about_contribution_tab_title">Contribuir</string> <string name="about_license_tab_title">Licencia</string> - - <string name="copied_to_clipboard">Copiado al portapapeles</string> - <string name="seconds_ago">hace unos segundos</string> <string name="edit">Editar</string> <string name="label_labels">Seleccionar etiquetas</string> @@ -113,6 +111,7 @@ <string name="no_lists_yet">Aún no hay listas</string> <string name="do_you_want_to_save_your_changes">¿Desea guardar los cambios?</string> <string name="do_you_want_to_archive_all_cards_of_the_list">¿Desea archivar todas las tarjetas de %1$s?</string> + <string name="do_you_want_to_archive_all_cards_of_the_filtered_list">¿Desea archivar todas las tarjetas filtradas de %1$s?</string> <plurals name="do_you_want_to_delete_the_current_list"> <item quantity="one">Esto eliminará permanentemente %1$d tarjeta de esta lista.</item> <item quantity="other">Esto eliminará permanentemente todas las %1$d tarjetas de esta lista.</item> @@ -129,6 +128,7 @@ <string name="delete_board_message">Esto borrará permanente el tablero, incluyendo todas las listas y tarjetas.</string> <string name="settings_theme_title">Tema oscuro</string> <string name="settings_branding_title">Branding</string> + <string name="settings_compact_title">Modo compacto</string> <string name="settings_background_sync">Sincronización en segundo plano</string> <string name="pref_value_wifi_and_mobile">Sincronizar sobre Wi-Fi y datos móviles</string> <string name="pref_value_wifi_only">Sincronizar solo sobre Wi-Fi</string> @@ -156,7 +156,6 @@ <string name="maintenance_mode_explanation">El servidor %1$s está actualmente en modo de mantenimiento. Por favor, contacta con tu administrador o vuelve a intentarlo más tarde.</string> <string name="share_add_to_card">Añadir a la tarjeta</string> <string name="share_success">Añadido satisfactoriamente %1$s a %2$s</string> - <string name="could_not_copy_to_clipboard">No se puede copiar al portapapeles</string> <string name="add_comment">Agregar comentario</string> <string name="card_edit_comments">Comentarios</string> <string name="no_comments_yet">Todavía no hay comentarios.</string> @@ -216,8 +215,8 @@ <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_might_help">Si el problema persiste, intenta limpiar borrar el almacenamiento de ambas apps, Nextcloud y Nextcloud Deck para solucionar este problema.</string> + <string name="error_dialog_tip_database_upgrade_failed">Falló la actualización de la base de datos. Por favor, informe del problema y limpie el almacenamiento para usar la aplicación de manera normal.</string> <string name="error_dialog_tip_clear_storage">Puedes limpiar el almacenamiento abriendo la información de la app y seleccionando Almacenamiento → Eliminar datos.</string> <string name="error_dialog_tip_files_outdated">Su aplicación Nextcloud parece que está desactualizada. Por favor visite la Play Store o F-Droid para conseguir la versión más reciente.</string> <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> @@ -231,6 +230,8 @@ <string name="error_dialog_we_need_info">Necesitamos la siguiente información técnica para ayudarle:</string> <string name="error_dialog_redirect">Tu servidor respondió con el código de estado HTTP 302, lo que implica que no está instalada la aplicación Deck en su servidor o que algo está mal configurado. Esto puede estar causado por anulaciones personalizadas en un archivo .htaccess o por aplicaciones de Nexcloud como OID Client. </string> <string name="error_dialog_version_not_parsable">No se pudo determinar la versión de la aplicación Deck en el lado del servidor. Por favor, compruebe que está instalada y activada. </string> + <string name="error_dialog_account_might_not_be_authorized">La cuenta de tu app Nextcloud puede ya no estar autorizada.</string> + <string name="error_dialog_user_not_found_in_database">El usuario actual no coincide con el usuario que tenemos en nuestra base de datos. Si estás usando LDAP en tu instancia de Nextcloud, tu app de Nextcloud puede haber almacenado una ID de usuario antigua.</string> <string name="error_dialog_capabilities_not_parsable">No se ha podido comprobar las capacidades de su servidor. Por favor compruebe que su servidor está funcionando bien y que otras aplicaciones de cliente son capaces de acceder a Nextcloud.</string> <string name="error_dialog_attachment_upload_failed">No se ha podido subir un adjunto. Por favor, intenta compartirlo de otra forma y comunícanos este fallo.</string> <string name="error_dialog_tip_disable_battery_optimizations">Por favor, desactiva todas las optimizaciones de la batería para Nextcloud y la aplicación Deck.</string> @@ -243,6 +244,7 @@ <string name="info_box_version_not_supported">La versión %1$s del servidor no está soportada. Por favor, actualiza a %2$s</string> <string name="share_link">Compartir enlace</string> <string name="archive_cards">Tarjetas archivadas</string> + <string name="manage_accounts">Gestionar cuentas</string> <string name="manage_list">Administrar su lista</string> <string name="simple_reply">Responder</string> @@ -251,9 +253,37 @@ <string name="add_text_as_comment">Añadir como comentario</string> <string name="progress_count">%1$d de %2$d</string> <plurals name="progress_error_count"> - <item quantity="one">%1$d error al subir.</item> - <item quantity="other">%1$d errores al subir.</item> + <item quantity="one">%1$d error al subir</item> + <item quantity="other">%1$d errores al subir</item> </plurals> <string name="simple_report">Informe</string> <string name="error_action_open_battery_settings">Ajustes de batería</string> + <string name="move_warning">No se pueden transferir cio los comentarios ni los adjuntos al mover una tarjeta a otro tablero.</string> + <string name="clone_board">Clonar tablero</string> + <string name="cloning_board">Clonando %1$s…</string> + <string name="successfully_cloned_board">Clonados con éxito %1$s</string> + <string name="attachment_does_not_yet_exist">El adjunto todavía no existe en Deck</string> + <string name="card_does_not_yet_exist">La tarjeta todavía no existe en Deck</string> + + <string name="widget_stack_title">Lista</string> + <string name="widget_stack_header_icon">Icono de la cabecera del widget</string> + <string name="widget_stack_placeholder_icon">Espacio para icono del widget</string> + <string name="select_stack">Seleccione lista</string> + <string name="project_type_deck_board">Tablero Deck</string> + <string name="project_type_deck_card">Tarjeta Deck</string> + <string name="project_type_file">Archivo</string> + <string name="projects_title">Proyectos</string> + <plurals name="resources_count"> + <item quantity="one">%1$d recurso</item> + <item quantity="other">%1$d recursos</item> + </plurals> + <string name="no_assigned_label">Sin etiqueta asignada</string> + <string name="single_card">Carta única</string> + <string name="project_type_room">Sala de conversación</string> + <string name="simple_move">Mover</string> + <string name="cannot_upload_files_without_permission">No se puede subir archivos sin permiso</string> + <string name="clone_cards">Clonar tarjetas</string> + <string name="simple_clone">Clonar</string> + <string name="user_avatar">Usar avatar</string> + <string name="simple_unassign">Sin asignar</string> </resources> diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index 2f6ccb012..283d409a3 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -28,7 +28,7 @@ <string name="simple_discard">baztertu</string> <string name="simple_update">Eguneratu</string> <string name="simple_delete">Ezabatu</string> - <string name="simple_rename">Berrizendatu</string> + <string name="simple_rename">Aldatu izena</string> <string name="simple_settings">Ezarpenak</string> <string name="simple_undo">Desegin</string> <string name="simple_manage">kudeatu</string> @@ -38,6 +38,7 @@ <string name="simple_disabled">desgaituta</string> <string name="simple_copied">Kopiatuta</string> <string name="simple_archive">Artxibatu</string> + <string name="simple_unassigned">Esleitu gabea</string> <string name="edit_board">Editatu mahaia</string> <string name="archive_board">Artxibatu mahaia</string> @@ -70,9 +71,6 @@ <string name="about_credits_tab_title">Kredituak</string> <string name="about_contribution_tab_title">Lagundu!</string> <string name="about_license_tab_title">Lizentzia</string> - - <string name="copied_to_clipboard">Kopiatu arbelera</string> - <string name="seconds_ago">segundo lehenago</string> <string name="edit">Editatu</string> <string name="label_labels">Hautatu etiketak</string> @@ -86,9 +84,9 @@ <string name="choose_account">Aukeratu kontua</string> <string name="add_card">Gehitu txartel berria</string> <string name="activity">Jarduera</string> - <string name="add_list">Zerrenda gehitu</string> - <string name="rename_list">Zerrenda berrizendatu</string> - <string name="delete_list">Zerrenda ezabatu</string> + <string name="add_list">Gehitu zerrenda</string> + <string name="rename_list">Aldatu izena zerrendari</string> + <string name="delete_list">Ezabatu zerrenda</string> <string name="label_menu">menua</string> <string name="action_card_assign">Esleitu txartel hau niri</string> <string name="action_card_unassign">Txartel hau niri esleitzeari utzi</string> @@ -123,12 +121,13 @@ </plurals> <string name="add_a_new_list_using_the_button">Gehitu zerrenda berria + botoia erabiliz</string> <string name="add_a_new_card_using_the_button">Gehitu txartel berria + botoia erabiliz</string> - <string name="update_deck">Eguneratu sorta</string> - <string name="your_deck_version_is_too_old">Zure sorta bertsioa zaharregia da</string> - <string name="deck_outdated_please_update">Zure sorta bertsioa zaharregia da (%1$s). Mesedez eguneratu ezazu android aplikazio hau bezero gisa erabiltzeko.</string> + <string name="update_deck">Eguneratu Deck</string> + <string name="your_deck_version_is_too_old">Zure Deck bertsioa zaharregia da</string> + <string name="deck_outdated_please_update">Zure Deck bertsioa zaharregia da (%1$s). Mesedez eguneratu ezazu android aplikazio hau bezero gisa erabiltzeko.</string> <string name="delete_board_message">Honek mahai hau eta bertako zerrenda eta txartel guztiak ezabatuko ditu.</string> <string name="settings_theme_title">Gai iluna</string> <string name="settings_branding_title">Marka</string> + <string name="settings_compact_title">Modu trinkoa</string> <string name="settings_background_sync">Atzeko planoko sinkronizazioa</string> <string name="pref_value_wifi_and_mobile">Sinkronizatu Wi-Fi eta datu mugikorrekin</string> <string name="pref_value_wifi_only">Sinkronizatu Wi-Fiarekin soilik</string> @@ -148,7 +147,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> @@ -156,12 +155,11 @@ <string name="maintenance_mode_explanation">%1$s zerbitzaria mantentze moduan dago. Mesedez jarri harremanetan zure administratzailearekin edo saiatu berriro.</string> <string name="share_add_to_card">Gehitu txartelera</string> <string name="share_success">%1$s ondo gehitu da %2$s -ra</string> - <string name="could_not_copy_to_clipboard">Ezin izan da arbelera kopiatu</string> <string name="add_comment">Gehitu iruzkina</string> <string name="card_edit_comments">Iruzkinak</string> <string name="no_comments_yet">Iruzkinik ez oraindik</string> <string name="no_boards">Ez dago mahairik oraindik</string> - <string name="add_a_new_board_using_the_button">Gehitu mahai berri bat + botoia erabiliz</string> + <string name="add_a_new_board_using_the_button">Gehitu mahai berria + botoia erabiliz</string> <string name="choose_board">Aukeratu mahaia</string> <string name="choose_list">Aukeratu zerrenda</string> <string name="task_count">%2$s(e)tik %1$s</string> @@ -199,8 +197,8 @@ <string name="filter_duedate_title">Epe-muga</string> <!-- Archived boards --> - <string name="action_board_dearchive">Desegin mahaiaren artxiboa</string> - <string name="archived_boards">Artxibatutako taulak</string> + <string name="action_board_dearchive">Desegin mahaia artxibatzea</string> + <string name="archived_boards">Artxibatutako mahaiak</string> <!-- Errors --> <string name="error">Errore bat agertu da</string> @@ -216,20 +214,23 @@ <string name="error_dialog_title">Oh ez - Orain zer? 🙁</string> <string name="error_dialog_tip_token_mismatch_retry">Saia zaitez aplikazioa itxi eta berrabiarazten. Agian Nextcloud aplikazioarekin gaizki konektatzea gertatu da.</string> - <string name="error_dialog_tip_token_mismatch_clear_storage">Arazoak jarraitzen badu, saiatu bi aplikazioen biltegiak garbitzen: Nextcloud eta Nextcloud Sortak aplikazioenak arazoa konpontzeko.</string> + <string name="error_dialog_tip_clear_storage_might_help">Arazoak jarraitzen badu, saiatu bi aplikazioen biltegiak garbitzen: Nextcloud eta Nextcloud Deck aplikazioenak, arazoa konpontzeko.</string> + <string name="error_dialog_tip_database_upgrade_failed">Datu-basearen bertsio-berritzeak huts egin du. Eman arazoaren berri eta biltegia garbitu aplikazioa normaltasunez erabiltzeko.</string> <string name="error_dialog_tip_clear_storage">Biltegia garbi dezakezu aplikazioaren informazioa ireki eta hautatuz: Biltegia → Garbitu biltegia.</string> <string name="error_dialog_tip_files_outdated">Daukazun Nextcloud aplikazioa zaharkituta dagoela dirudi. Zoaz Play Store edo F-Droidera azken bertsioa lortzeko.</string> - <string name="error_dialog_tip_files_force_stop">Daukazun Nextcloud aplikazioan zerbait gaizki dabilela dirudi. Saia zaitez Nextcloud aplikazioa eta Nextcloud Sortak geldiarazten. </string> + <string name="error_dialog_tip_files_force_stop">Daukazun Nextcloud aplikazioan zerbait gaizki dabilela dirudi. Saia zaitez Nextcloud aplikazioa eta Nextcloud Deck geldiarazten. </string> <string name="error_dialog_tip_files_delete_storage">Geldiarazteak ez badu arazoa konpontzen, bi biltegiak garbitzen saia zaitezke.</string> <string name="error_dialog_timeout_instance">Ez da zerbitzariaren erantzunik jaso emandako denboran. Ziurta ezazu zure instantzia ondo dabilela.</string> <string name="error_dialog_timeout_toggle">Egiaztatu zure datu konexioa. Batzuetan mugikorraren datuak edo WI-FI konexioa itzali eta piztea lagungarri da.</string> - <string name="error_dialog_check_server">Zerbitzariaren erantzuna ez da zuzena izan. Egiazta ezazu Sortak aplikazioa atzitu dezakezula web interfazearen bidez.</string> + <string name="error_dialog_check_server">Zerbitzariaren erantzuna ez da zuzena izan. Egiazta ezazu Deck aplikazioa atzitu dezakezula web interfazearen bidez.</string> <string name="error_dialog_check_server_logs">Arazo bat dago zure Nextcloud ezarpenekin. Eman begiratu bat zerbitzariko egunkari-fitxategiei.</string> <string name="error_dialog_check_maintenance">Egiazta ezazu zure Nextcloud instantzia ez dagoela mantentze moduan une honetan.</string> <string name="error_dialog_insufficient_storage">Zure Nextcloud instantziak ez du biltegiratze leku libre gehiagorik. Ezabatu fitxategi batzuk, aldaketa lokalak hodeiarekin sinkroniza daitezen.</string> <string name="error_dialog_we_need_info">Zuri laguntzeko hurrengo informazio teknikoa behar dugu:</string> - <string name="error_dialog_redirect">Zure zerbitzariak HTTP 302 egoera kodearekin erantzun du, honek esan nahi du ez duzula Sortak aplikazioa ez duzula zure zerbitzarian instalatu edo zerbait txarto konfiguratuta dagoela. Hau .htaccess fitxategi batean arau pertsonalizatuak daudelako edo OID Bezeroa izeneko aplikazioarengatik sortua izan daiteke. </string> + <string name="error_dialog_redirect">Zure zerbitzariak HTTP 302 egoera kodearekin erantzun du, honek esan nahi du ez duzula Deck aplikazioa ez duzula zure zerbitzarian instalatu edo zerbait txarto konfiguratuta dagoela. Hau .htaccess fitxategi batean arau pertsonalizatuak daudelako edo OID Bezeroa izeneko aplikazioarengatik sortua izan daiteke. </string> <string name="error_dialog_version_not_parsable">Ezin izan dugu zerbitzariaren Deck aplikazioaren bertsioa zehaztu. Ziurtatu instalatuta eta gaituta dagoela.</string> + <string name="error_dialog_account_might_not_be_authorized">Baliteke Nextcloud aplikazioaren kontua baimenduta ez egotea.</string> + <string name="error_dialog_user_not_found_in_database">Egungo erabiltzailea ez dator bat gure datu-basean daukagun erabiltzailearekin. Zure Nextcloud instantzian LDAP erabiltzen ari bazara, baliteke zure Nextcloud aplikazioak erabiltzaile-ID zahar bat biltegiratu izana.</string> <string name="error_dialog_capabilities_not_parsable">Ezin izan ditugu zure zerbitzariko gaitasunak eskuratu. Ziurtatu zure zerbitzaria badabilela eta beste bezero aplikazioak Nexcloud atzitzeko gai direla.</string> <string name="error_dialog_attachment_upload_failed">Eranskin bat ezin izan da igo. Parteka ezazu beste bide batetik eta emaiguzu akats honen berri, mesedez.</string> <string name="error_dialog_tip_disable_battery_optimizations">Desgaitu bateria optimizazio guztiak Nextcloudentzat eta Deck aplikazioarentzat.</string> @@ -242,12 +243,42 @@ <string name="info_box_version_not_supported">Zerbitzariaren %1$s bertsioa ez dago onartuta, mesedez eguneratu %2$s(e)ra</string> <string name="share_link">Partekatu esteka</string> <string name="archive_cards">Artxibatu txartelak</string> + <string name="manage_accounts">Kudeatu kontuak</string> <string name="manage_list">Kudeatu zerrenda</string> <string name="simple_reply">Erantzun</string> <string name="error_while_uploading_attachment">Errorea eranskin hau igotzean: %1$s</string> <string name="append_text_to_description">Erantsi deskripzioari</string> <string name="add_text_as_comment">Gehitu iruzkin gisa</string> + <string name="progress_count">%2$dtik%1$d</string> + <plurals name="progress_error_count"> + <item quantity="one">%1$derrore kargatzerakoan</item> + <item quantity="other">%1$derrore kargatzerakoan</item> + </plurals> <string name="simple_report">Jakinarazi</string> <string name="error_action_open_battery_settings">Bateria ezarpenak</string> -</resources> + <string name="move_warning">Ez iruzkinak eta ez eranskinak ezin izan dira transferitu txartela beste mahai batera mugitzean.</string> + <string name="clone_board">Klonatu mahaia</string> + <string name="cloning_board">%1$sklonatzen...</string> + <string name="successfully_cloned_board">%1$sondo klonatu da</string> + <string name="attachment_does_not_yet_exist">Eranskina oraindik ez dago Deck-en</string> + <string name="card_does_not_yet_exist">Txartela oraindik ez dago Deck-en</string> + + <string name="widget_stack_title">Zerrenda</string> + <string name="widget_stack_header_icon">Goiburu widgetaren ikonoa</string> + <string name="widget_stack_placeholder_icon">Leku-marka widgetaren ikonoa</string> + <string name="select_stack">Hautatu zerrenda</string> + <string name="project_type_deck_board">Deck mahaia</string> + <string name="project_type_deck_card">Deck txartela</string> + <string name="project_type_file">Fitxategia</string> + <string name="projects_title">Proiektuak</string> + <plurals name="resources_count"> + <item quantity="one">%1$dbaliabide</item> + <item quantity="other">%1$dbaliabide</item> + </plurals> + <string name="no_assigned_label">Esleitutako etiketarik ez</string> + <string name="single_card">Txartel bakana</string> + <string name="project_type_room">Hitz egiteko gela</string> + <string name="simple_move">Mugitu</string> + <string name="cannot_upload_files_without_permission">Ezin da fitxategirik kargatu baimenik gabe</string> + </resources> diff --git a/app/src/main/res/values-fi-rFI/strings.xml b/app/src/main/res/values-fi-rFI/strings.xml index 3847d85a8..02d5c3423 100644 --- a/app/src/main/res/values-fi-rFI/strings.xml +++ b/app/src/main/res/values-fi-rFI/strings.xml @@ -21,6 +21,7 @@ <string name="simple_exception">Poikkeus</string> <string name="simple_close">sulje</string> <string name="simple_open">Avoinna</string> + <string name="simple_switch">Kytkin</string> <string name="simple_filter">Suodata</string> <string name="simple_overdue">Myöhässä</string> <string name="simple_clear">Tyhjennä</string> @@ -36,6 +37,8 @@ <string name="simple_comment">Kommentti</string> <string name="simple_disabled">poistettu käytöstä</string> <string name="simple_copied">Kopioitu</string> + <string name="simple_archive">Arkisto</string> + <string name="simple_unassigned">Määrittämätön</string> <string name="edit_board">Muokkaa taulua</string> <string name="archive_board">Arkistoi taulu</string> @@ -68,16 +71,21 @@ <string name="about_credits_tab_title">Tekijät</string> <string name="about_contribution_tab_title">Kontribuutio</string> <string name="about_license_tab_title">Lisenssi</string> - <string name="seconds_ago">sekuntia sitten</string> <string name="edit">Muokkaa</string> <string name="label_labels">Valitse tunnisteet</string> <string name="label_description">Kuvaus</string> <string name="hint_assign_people">Määritä käyttäjät</string> <string name="hint_due_date_date">Eräpäivä</string> + <string name="card_not_found">Korttia ei löydy</string> + <string name="card_not_found_message">Korttia ei löytynyt. Se voi olla äskettäin poistettu.</string> + <string name="add_account">Lisää tili</string> <string name="choose_account">Valitse tili</string> + <string name="add_card">Lisää uusi kortti</string> <string name="activity">Aktiviteetti</string> + <string name="add_list">Lisää lista</string> + <string name="rename_list">Nimeä lista uudelleen</string> <string name="delete_list">Poista lista</string> <string name="label_menu">Valikko</string> <string name="action_card_assign">Määritä kortti minulle</string> @@ -89,13 +97,6 @@ <string name="label_clear_due_date">Poista eräpäivä</string> <string name="label_add">Lisää %1$s</string> - <!-- Error messages --> - <string name="error">Tapahtui virhe</string> - <string name="copied_to_clipboard">Kopioitu leikepöydälle</string> - <string name="maintenance_mode">Palvelin on huoltotilassa</string> - <string name="server_misconfigured">Virheelliset palvelimen asetukset</string> - <string name="server_misconfigured_explanation">Palvelimesi vastasi HTTP 302 tilakoodilla, joka tarkoittaa, että Deck/pakka sovellusta ei ole asennettu palvelimellesi tai jossain on virheelliset asetukset. Tämän voi aiheuttaa muunneltu .htaccess tiedosto tai Nextcloud sovellus kuten OID asiakas. Tukipalvelun yhteystiedot löytyvät muuta valikosta. </string> - <string name="server_error">Palvelinvirhe</string> <string name="card_edit_details">Tiedot</string> <string name="card_edit_attachments">Liitteet</string> <string name="card_edit_activity">Tapahtumat</string> @@ -107,12 +108,26 @@ <string name="account_already_added">Tili on jo lisätty</string> <string name="account_is_getting_imported">Tiliä tuodaan sisään</string> <string name="not_synced_yet">Ei vielä synkronoitu</string> + <string name="no_lists_yet">Ei vielä listoja</string> <string name="do_you_want_to_save_your_changes">Haluatko tallentaa muutokset?</string> + <string name="do_you_want_to_archive_all_cards_of_the_list">Haluatko arkistoida kaikki kortit listalta %1$s?</string> + <plurals name="do_you_want_to_delete_the_current_list"> + <item quantity="one">Tämä poistaa %1$d kortin tältä listalta.</item> + <item quantity="other">Tämä poistaa kaikki %1$d korttia tältä listalta.</item> + </plurals> + <plurals name="do_you_want_to_delete_the_label"> + <item quantity="one">Tämä poistaa nimikkeen %1$d kortilta.</item> + <item quantity="other">Tämä poistaa nimikkeen %1$d kortilta.</item> + </plurals> + <string name="add_a_new_list_using_the_button">Lisää uusi lista painamalla +</string> <string name="add_a_new_card_using_the_button">Lisää uusi kortti painamalla +</string> <string name="update_deck">Päivitä pakka</string> <string name="your_deck_version_is_too_old">Pakan versio on liian vanha</string> - <string name="deck_outdated_please_update">Pakan versio on liian vanha. Päivitä kayttämällä tätä android sovellusta asiakkaana.</string> + <string name="deck_outdated_please_update">Deck-versiosi (%1$s) on liian vanha. Päivitä sovellus.</string> + <string name="delete_board_message">Tämä poistaa tämän taulun ja sen listat ja kortit pysyvästi.</string> <string name="settings_theme_title">Tumma teema</string> + <string name="settings_branding_title">Brändäys</string> + <string name="settings_compact_title">Kompakti tila</string> <string name="settings_background_sync">Taustasynkronointi</string> <string name="pref_value_wifi_and_mobile">Synkronoi wifi- ja mobiilidatayhteydellä</string> <string name="pref_value_wifi_only">Synkronoi vain wifi-yhteydellä</string> @@ -137,11 +152,24 @@ <string name="provide_at_least_a_title_or_description">Anna vähintään otsikko tai kuvaus</string> <string name="welcome_text">Tervetuloa kohteeseen %1$s</string> <string name="save_card_before_attachment">Kortti pitää tallentaa ennen kuin liitetiedostoja voidaan lisätä.</string> + <string name="maintenance_mode_explanation">Palvelin%1$s on juuri ylläpitotilassa. Ota yhteys järjestelmän ylläpitäjään tai yritä myöhemmin uudelleen.</string> <string name="share_add_to_card">Lisää kortille</string> <string name="share_success">Onnistuneesti lisätty %1$s kohteeseen %2$s</string> - <string name="could_not_copy_to_clipboard">Ei voitu kopioida leikepöydälle</string> + <string name="add_comment">Lisää kommentti</string> <string name="card_edit_comments">Kommentit</string> + <string name="no_comments_yet">Ei kommentteja</string> + <string name="no_boards">Ei vielä tauluja</string> + <string name="add_a_new_board_using_the_button">Lisää uusi taulu painamalla +</string> + <string name="choose_board">Valitse taulu</string> + <string name="choose_list">Valitse lista</string> + <string name="task_count">%1$s/%2$s</string> <string name="open_in_browser">Avaa selaimessa</string> + <string name="updating_card">Päivitetään korttia...</string> + + <!-- Move lists --> + <string name="move_list_right">Siirrä lista oikealle</string> + <string name="move_list_left">Siirrä lista vasemmalle</string> + <!-- Filter --> <string name="filter_no_filter">Kaikki</string> <string name="filter_overdue">Myöhässä</string> @@ -150,6 +178,104 @@ <string name="filter_month">Seuraavat 30 päivää</string> <string name="filter_no_due">Ei eräpäivää</string> <string name="filter_by_tag">Suodata tunnisteen perusteella</string> + <string name="filter_by_assigned_user">Suodata määritetyn käyttäjän mukaan</string> + <string name="filter_by_duedate">Suodata määräpäivän mukaan</string> + <!-- Archived cards --> <string name="archived_cards">Arkistoidut kortit</string> + <string name="action_card_dearchive">Kumoa kortin arkistointi</string> + <string name="action_archived_cards">Selaa arkistoituja kortteja</string> + <string name="attachment_already_exists">Liite on jo olemassa</string> + <string name="pick_custom_color">Valitse mukautettu väri</string> + <string name="manage_tags">Hallitse tunnisteita</string> + <string name="add_tag">Lisää tunniste</string> + <string name="tag_already_exists">%1$s on jo olemassa</string> + <string name="tag_successfully_added">%1$s lisätty onnistuneesti</string> + <string name="edit_tag">Muokkaa tunnistetta %1$s</string> + <string name="filter_tags_title">Tunnisteet</string> + <string name="filter_user_title">Käyttäjät</string> + <string name="filter_duedate_title">Eräpäivä</string> + + <!-- Archived boards --> + <string name="action_board_dearchive">Kumoa taulun arkistointi</string> + <string name="archived_boards">Arkistoidut taulut</string> + + <!-- Errors --> + <string name="error">Tapahtui virhe</string> + <string name="synchronization_failed">Synkronointi epäonnistui</string> + <string name="operation_not_yet_supported">Ei vielä tuettu</string> + <string name="error_revoking_ac">Virhe käyttöoikeuden kumoamisessa (%1$s)</string> + <string name="error_create_label">Virhe nimikkeen luonnissa (%1$s)</string> + <string name="maintenance_mode">Palvelin on huoltotilassa</string> + <string name="server_misconfigured">Virheelliset palvelimen asetukset</string> + <string name="server_error">Palvelinvirhe</string> + <string name="shared_error">Sisällön jako 3. osapuolen sovelluksista ei ole vielä täysin tuettu. Lataa kohde ensin ja jaa se laitteen tiedostohallinnasta tai galleriasta.</string> + <string name="error_edit_activity_killed_by_android">Android päätti muokkaustilan, koska resursseja tarvitaan muille sovelluksille.</string> + + <string name="error_dialog_title">Voi ei - Mitä nyt? 🙁</string> + <string name="error_dialog_tip_token_mismatch_retry">Yritä pakottaa sovelluksen sulkeminen ja käynnistä se uudelleen. Nextcloud-sovellukseen on saattanut olla virheellinen yhteys.</string> + <string name="error_dialog_tip_clear_storage_might_help">Jos ongelma jatkuu, tyhjennä Nextcloudin ja Nextcloud Deckin tallennustila laitteesta ratkaistaaksesi ongelman.</string> + <string name="error_dialog_tip_database_upgrade_failed">Tietokannan päivittäminen epäonnistui. Ole hyvä ja raportoi ongelmasta ja tyhjennä tallennustila käyttääksesi sovellusta normaalisti.</string> + <string name="error_dialog_tip_clear_storage">Voit tyhjentää tallennustilan avaamalla sovelluksen tiedot ja valitsemalla Tallennustila → Tyhjennä tallennustila.</string> + <string name="error_dialog_tip_files_outdated">Nextcloud-sovelluksesi on vanhentunut. Lataa uusin version Play Storesta tai F-Droidista.</string> + <string name="error_dialog_tip_files_force_stop">Nextcloud-sovelluksessasi on jotain vialla. Pakota Nextcloudin ja Nextcloud Deckin lopetus.</string> + <string name="error_dialog_tip_files_delete_storage">Jos pakotettu lopettaminen ei auta, voit yrittää tyhjentää sovelluksien tallennustilan.</string> + <string name="error_dialog_timeout_instance">Palvelimen aikakatkaisu. Varmista, että se on toiminnassa.</string> + <string name="error_dialog_timeout_toggle">Tarkista verkkoyhteytesi. Joskus mobiilidatan tai Wi-Fin kytkeminen pois päältä ja päälle saattaa ratkaista ongelman.</string> + <string name="error_dialog_check_server">Palvelin vastasi virheellisesti. Tarkista, saatko yhteyden Deckiin selaimen kautta.</string> + <string name="error_dialog_check_server_logs">Nextcloud-asennuksessasi on ongelma. Katso lisätietoja palvelimen lokitiedostoista.</string> + <string name="error_dialog_check_maintenance">Varmista, ettei Nextcloud-palvelin ole ylläpitotilassa.</string> + <string name="error_dialog_insufficient_storage">Nextcloud-palvelimellasi ei ole vapaata tallennustilaa. Poista tiedostoja, jotta paikallisten muutosten synkronointi pilveen onnistuu.</string> + <string name="error_dialog_we_need_info">Tarvitsemme seuraavat tekniset tiedot auttaaksemme sinua:</string> + <string name="error_dialog_redirect">Palvelimesi vastasi HTTP-tilakoodilla 302, joka viittaa siihen, että Deckiä ei ole asennettu tai se on määritelty virheellisesti. Syynä saattaa olla muokattu .htaccess-tiedosto tai jokin Nextcloud-sovellus, kuten OID Client.</string> + <string name="error_dialog_version_not_parsable">Palvelimen Deck-sovelluksen versiota ei voitu määrittää. Varmista, että se on asennettu ja otettu käyttöön.</string> + <string name="error_dialog_account_might_not_be_authorized">Tilisi valtuutus Nextcloud-sovellukseen saattaa olla päättynyt.</string> + <string name="error_dialog_user_not_found_in_database">Käyttäjää ei löydy tietokannastamme. Jos LDAP on käytössä Nextcloudissa, Nextcloud-sovellksessa saattaa olla tallennettuna vanha käyttäjän tunnus.</string> + <string name="error_dialog_capabilities_not_parsable">Palvelimesi ominaisuuksia ei voitu hakea. Varmista, että palvelin on käynnissä ja muut asiakasohjelmat saavat yhteyden Nextcloudiin.</string> + <string name="error_dialog_attachment_upload_failed">Liitettä ei voitu lähettää. Yritä jakaa se toista kautta ja kerro meille tästä bugista.</string> + <string name="error_dialog_tip_disable_battery_optimizations">Ota pois käytöstä akun optimointi Nextcloud- ja Deck-sovelluksilta.</string> + <string name="error_action_open_deck_info">Avaa sovelluksen tiedot</string> + <string name="error_action_open_network">Verkkoasetukset</string> + <string name="error_action_server_logs">Palvelimen lokit</string> + <string name="error_action_report_issue">Raportti</string> + <string name="info_box_maintenance_mode">Palvelin on huoltotilassa</string> + <string name="info_box_version_not_supported">Palvelinversio %1$s ei ole tuettu, päivitä versioon %2$s</string> + <string name="share_link">Jaa linkki</string> + <string name="archive_cards">Arkistoi kortit</string> + + <string name="manage_accounts">Tilien hallinta</string> + <string name="manage_list">Hallinnoi listaa</string> + <string name="simple_reply">Vastaa</string> + <string name="error_while_uploading_attachment">Virhe liitteen lähttämisessä: %1$s</string> + <string name="append_text_to_description">Lisää selitteeseen</string> + <string name="add_text_as_comment">Lisää kommenttina</string> + <string name="progress_count">%1$d / %2$d</string> + <plurals name="progress_error_count"> + <item quantity="one">%1$d virhe lähettämisen aikana</item> + <item quantity="other">%1$d virhettä lähettämisen aikana</item> + </plurals> + <string name="simple_report">Raportti</string> + <string name="error_action_open_battery_settings">Akkuasetukset</string> + <string name="move_warning">Kommentteja tai liitteitä ei siirretä, kun kortti siirretään toiseen tauluun.</string> + <string name="clone_board">Monista taulu</string> + <string name="cloning_board">Kloonataan %1$s…</string> + <string name="successfully_cloned_board">Taulun %1$s kloonaus onnistui</string> + <string name="attachment_does_not_yet_exist">Liitettä ei vielä ole Deckissä</string> + <string name="card_does_not_yet_exist">Korttia ei vielä ole Deckissä</string> + + <string name="widget_stack_title">Lista</string> + <string name="select_stack">Valitse lista</string> + <string name="project_type_deck_board">Deck-taulu</string> + <string name="project_type_deck_card">Deck-kortti</string> + <string name="project_type_file">Tiedosto</string> + <string name="projects_title">Projektit</string> + <plurals name="resources_count"> + <item quantity="one">%1$d resurssi</item> + <item quantity="other">%1$d resurssia</item> + </plurals> + <string name="no_assigned_label">Ei määritettyä tunnistetta</string> + <string name="single_card">Yksittäinen kortti</string> + <string name="project_type_room">Keskusteluhuone</string> + <string name="simple_move">Siirrä</string> + <string name="cannot_upload_files_without_permission">Tiedostojen lähettäminen ei ole mahdollista ilman käyttöoikeutta</string> </resources> diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 89fa234ee..9edbf2e36 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -20,9 +20,9 @@ <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_filter">Filtrer</string> <string name="simple_overdue">En retard</string> <string name="simple_clear">Effacer</string> <string name="simple_discard">ignorer</string> @@ -38,6 +38,7 @@ <string name="simple_disabled">désactivé</string> <string name="simple_copied">Copié</string> <string name="simple_archive">Archiver</string> + <string name="simple_unassigned">Non assigné(s)</string> <string name="edit_board">Modifier le tableau</string> <string name="archive_board">Archiver le tableau</string> @@ -47,7 +48,7 @@ <!-- About --> <string name="about">À propos de</string> <string name="about_version_title">Version</string> - <string name="about_version">Vous utilisez actuellement %1$s</string> + <string name="about_version">Vous utilisez actuellement la version : %1$s</string> <string name="about_maintainer_title">Responsable</string> <string name="about_developers_title">Développeurs</string> <string name="about_translators_title">Traducteurs</string> @@ -64,22 +65,19 @@ <string name="about_app_license">Cette application est sous licence GNU GENERAL PUBLIC LICENSE v3+.</string> <string name="about_app_license_button">Voir la licence</string> <string name="about_icons_disclaimer_title">Icônes</string> - <string name="about_icons_disclaimer_app_icon">Pour l’icône original voir %1$s.</string> + <string name="about_icons_disclaimer_app_icon">Pour l’icône originale voir %1$s.</string> <string name="about_icons_disclaimer_mdi_icons">Les icônes suivantes utilisées dans cette applications sont %1$s réalisées par Google Inc. et sous licence Apache 2.0.</string> <string name="about_icons_disclaimer_mdi">Icônes Material Design</string> <string name="about_credits_tab_title">Crédits</string> <string name="about_contribution_tab_title">Contribution</string> <string name="about_license_tab_title">Licence</string> - - <string name="copied_to_clipboard">Copié dans le presse-papier</string> - <string name="seconds_ago">à l\'instant</string> <string name="edit">Modifier</string> <string name="label_labels">Sélectionner les étiquettes</string> <string name="label_description">Description</string> <string name="hint_assign_people">Assigner des utilisateurs</string> <string name="hint_due_date_date">Date d\'échéance</string> - <string name="card_not_found">Carte non trouvé</string> + <string name="card_not_found">Carte non trouvée</string> <string name="card_not_found_message">La carte est introuvable. Elle a peut-être été supprimée récemment.</string> <string name="add_account">Ajouter un compte</string> @@ -112,7 +110,7 @@ <string name="not_synced_yet">Aucune synchronisation pour l\'instant</string> <string name="no_lists_yet">Aucune liste pour l\'instant</string> <string name="do_you_want_to_save_your_changes">Souhaitez-vous enregistrer vos modifications ?</string> - <string name="do_you_want_to_archive_all_cards_of_the_list">Voulez vous archiver toutes les carte de %1$s ?</string> + <string name="do_you_want_to_archive_all_cards_of_the_list">Souhaitez-vous archiver toutes les carte de %1$s ?</string> <plurals name="do_you_want_to_delete_the_current_list"> <item quantity="one">Cela supprimera définitivement l\'ensemble des %1$d cartes de cette liste.</item> <item quantity="other">Cela supprimera définitivement l\'ensemble des %1$d cartes de cette liste.</item> @@ -125,10 +123,11 @@ <string name="add_a_new_card_using_the_button">Ajouter une nouvelle carte en utilisant le bouton +.</string> <string name="update_deck">Mettre à jour Deck</string> <string name="your_deck_version_is_too_old">Votre version de Deck est trop ancienne</string> - <string name="deck_outdated_please_update">Votre version de deck est trop ancienne (%1$s). Veuillez mettre à jour pour utiliser cette application Android en tant que client.</string> - <string name="delete_board_message">Cette action supprimera définitivement ce tableau incluant ses listes et ses cartes.</string> + <string name="deck_outdated_please_update">Votre version de l\'application Deck est trop ancienne (%1$s). Veuillez la mettre à jour afin de pouvoir utiliser cette application Android en tant que client.</string> + <string name="delete_board_message">Cette action supprimera définitivement ce tableau, y compris la totalité de ses listes et de ses cartes.</string> <string name="settings_theme_title">Thème sombre</string> - <string name="settings_branding_title">Marque</string> + <string name="settings_branding_title">Thème du serveur</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> @@ -155,13 +154,12 @@ <string name="save_card_before_attachment">Vous devez enregistrer la carte avant d\'ajouter une pièce jointe.</string> <string name="maintenance_mode_explanation">Le serveur %1$s est actuellement en maintenance. Veuillez contacter votre administrateur ou réessayer plus tard.</string> <string name="share_add_to_card">Ajouter à la carte</string> - <string name="share_success">Ajout de %1$s à %2$s réussit</string> - <string name="could_not_copy_to_clipboard">Impossible de copier dans le presse-papiers</string> + <string name="share_success">Ajout de %1$s à %2$s réussi</string> <string name="add_comment">Ajouter un commentaire</string> <string name="card_edit_comments">Commentaires</string> <string name="no_comments_yet">Aucun commentaire pour l\'instant</string> <string name="no_boards">Pas encore de tableaux</string> - <string name="add_a_new_board_using_the_button">Ajouter un nouveau table en utilisant le bouton +</string> + <string name="add_a_new_board_using_the_button">Ajouter un nouveau tableau en utilisant le bouton +</string> <string name="choose_board">Choisir un tableau</string> <string name="choose_list">Sélectionner la liste</string> <string name="task_count">%1$s/%2$s</string> @@ -180,7 +178,7 @@ <string name="filter_month">30 prochains jours</string> <string name="filter_no_due">Sans échéance</string> <string name="filter_by_tag">Filtrer par étiquette</string> - <string name="filter_by_assigned_user">Filtrer par utilisateur</string> + <string name="filter_by_assigned_user">Filtrer par utilisateur assigné</string> <string name="filter_by_duedate">Filtrer par échéance</string> <!-- Archived cards --> @@ -215,24 +213,27 @@ <string name="error_edit_activity_killed_by_android">Android a mis fin au mode édition car il avait besoin de plus de ressources système pour les autres applications.</string> <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_token_mismatch_retry">Essayez de forcer la fermeture de l\'application puis de la redémarrer. Il y avait peut-être une mauvaise connexion à Nextcloud.</string> + <string name="error_dialog_tip_clear_storage_might_help">Si le problème persiste, essayez d\'effacer les données des 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 signaler l\'erreur et supprimer les données pour utiliser l\'application de manière normale.</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_outdated">Votre application Nextcloud semble trop ancienne. Veuillez visiter le Play Store ou F-Droid afin d\'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> <string name="error_dialog_tip_files_delete_storage">Si l’arrêt forcé ne fonctionne pas, vous pouvez essayer d\'effacer les données des deux applications.</string> <string name="error_dialog_timeout_instance">Le serveur n’a pas répondu dans le temps imparti. Assurez-vous que votre instance fonctionne correctement.</string> <string name="error_dialog_timeout_toggle">Vérifiez votre connexion réseau. L\'activation et la désactivation des données mobiles ou du Wi-Fi peuvent aider.</string> - <string name="error_dialog_check_server">Le serveur a renvoyé une réponse incorrecte. Vérifier si vous pouvez accéder à vos notes via l\'interface Web.</string> - <string name="error_dialog_check_server_logs">Il y a un problème avec votre configuration Nextcloud. Consulter les fichiers journaux du serveur.</string> - <string name="error_dialog_check_maintenance">Vérifier si votre instance Nextcloud n\'est pas actuellement en mode maintenance.</string> + <string name="error_dialog_check_server">Votre serveur a renvoyé une réponse incorrecte. Vérifiez si vous pouvez accéder à l\'application Deck via l\'interface Web.</string> + <string name="error_dialog_check_server_logs">Il y a un problème avec votre configuration Nextcloud. Consultez s\'il vous plaît les fichiers journaux du serveur.</string> + <string name="error_dialog_check_maintenance">Vérifiez s\'il vous plaît si votre instance Nextcloud n\'est actuellement pas en mode maintenance.</string> <string name="error_dialog_insufficient_storage">Votre instance Nextcloud n\'a plus d\'espace disponible. Veuillez supprimer certains fichiers pour pouvoir synchroniser vos modifications locales dans votre cloud.</string> <string name="error_dialog_we_need_info">Nous avons besoin des informations techniques suivantes pour pouvoir vous aider :</string> - <string name="error_dialog_redirect">Votre serveur a répondu avec le code d\'état HTTP 302, ce qui implique que vous n\'avez pas installé l\'application Deck sur votre serveur ou que quelque chose est mal configuré. Cela peut être dû à des modifications personnalisés du fichier .htaccess ou à des applications Nextcloud comme un client OID. Vous pouvez trouver nos coordonnées pour le support dans la section «À propos».</string> - <string name="error_dialog_version_not_parsable">Nous n\'avons pas pu déterminer la version de l\'app Deck côté serveur. Veuillez vérifier qu\'elle est bien installé et activé.</string> - <string name="error_dialog_capabilities_not_parsable">Nous n\'avons pas pu récupérer les capacités de votre serveur. Veuillez vous assurer que votre serveur fonctionne bien et que les autres apps client peuvent accéder à Nextcloud.</string> + <string name="error_dialog_redirect">Votre serveur a répondu avec le code d\'état HTTP 302, ce qui implique que vous n\'avez pas installé l\'application Deck sur votre serveur ou que quelque chose est mal configuré. Cela peut être dû à des modifications personnalisées du fichier .htaccess ou à des applications Nextcloud comme un client OID.</string> + <string name="error_dialog_version_not_parsable">Nous n\'avons pas pu déterminer la version de l\'application Deck sur votre serveur. Veuillez vérifier qu\'elle est bien installée et activée.</string> + <string name="error_dialog_account_might_not_be_authorized">Il semble possible que le compte de votre application Nextcloud ne soit plus autorisé.</string> + <string name="error_dialog_user_not_found_in_database">L\'utilisateur actuel est différent de l\'utilisateur que nous avons dans notre base de données. Si vous utilisez LDAP sur votre instance Nextcloud, il est alors possible que votre application Nextcloud ait stocké un identifiant utilisateur obsolète</string> + <string name="error_dialog_capabilities_not_parsable">Nous n\'avons pas pu évaluer les capacités de votre serveur. Veuillez vous assurer que votre serveur fonctionne bien et que les autres applications clientes peuvent accéder à Nextcloud.</string> <string name="error_dialog_attachment_upload_failed">Une pièce jointe n’a pas pu être téléversée. Veuillez essayer de la partager d’une autre façon et faites-nous part de ce bogue.</string> - <string name="error_dialog_tip_disable_battery_optimizations">Merci de bien vouloir désactiver toutes les optimisations de batterie pour Nextcloud et l\'application Deck.</string> + <string name="error_dialog_tip_disable_battery_optimizations">Merci de bien vouloir désactiver toutes les optimisations de batterie pour Nextcloud ainsi que pour l\'application Deck.</string> <string name="error_action_open_deck_info">Ouvrir les informations de l\'application</string> <string name="error_action_open_network">Paramètres réseau</string> <string name="error_action_server_logs">Journaux serveur</string> @@ -242,6 +243,7 @@ <string name="info_box_version_not_supported">Version du serveur %1$s non prise en charge, veuillez mettre à jour en %2$s</string> <string name="share_link">Lien de partage</string> <string name="archive_cards">Archiver les cartes</string> + <string name="manage_accounts">Gérer les comptes</string> <string name="manage_list">Gérer la liste</string> <string name="simple_reply">Répondre</string> @@ -250,9 +252,33 @@ <string name="add_text_as_comment">Ajouter en commentaire</string> <string name="progress_count">%1$d sur %2$d</string> <plurals name="progress_error_count"> - <item quantity="one">%1$derreur en mettant à jour.</item> - <item quantity="other">%1$derreurs en mettant à jour</item> + <item quantity="one">%1$d erreur lors de l\'envoi</item> + <item quantity="other">%1$d erreurs lors de l\'envoi</item> </plurals> <string name="simple_report">Signaler</string> <string name="error_action_open_battery_settings">Paramètres de batterie</string> -</resources> + <string name="move_warning">Ni les commentaires ni les pièces jointes ne peuvent être transférés lorsque la carte est déplacée sur un autre tableau.</string> + <string name="clone_board">Dupliquer le tableau</string> + <string name="cloning_board">Clonage de %1$s…</string> + <string name="successfully_cloned_board">Cloné avec succès %1$s</string> + <string name="attachment_does_not_yet_exist">Pièce jointe non encore présente dans le tableau</string> + <string name="card_does_not_yet_exist">Carte non encore présente dans le tableau</string> + + <string name="widget_stack_title">Liste</string> + <string name="widget_stack_header_icon">Icône d’en-tête du widget</string> + <string name="widget_stack_placeholder_icon">Icône d’espace réservé du widget</string> + <string name="select_stack">Sélectionner la liste</string> + <string name="project_type_deck_board">Tableau de l\'application Deck</string> + <string name="project_type_deck_card">Carte de l\'application Deck</string> + <string name="project_type_file">Fichier</string> + <string name="projects_title">Projets</string> + <plurals name="resources_count"> + <item quantity="one">%1$d ressource</item> + <item quantity="other">%1$d ressources</item> + </plurals> + <string name="no_assigned_label">Pas d’étiquette attribuée</string> + <string name="single_card">Carte unique</string> + <string name="project_type_room">Salon de discussion</string> + <string name="simple_move">Déplacer</string> + <string name="cannot_upload_files_without_permission">Impossible de téléverser des fichiers sans permission</string> + </resources> diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 6091b2f0b..d2e9ae886 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -6,7 +6,7 @@ <string name="drawer_current_account">Conta actual</string> <string name="drawer_middle_account">Conta media</string> <string name="drawer_end_account">Última conta</string> - <string name="drawer_manage_accounts">Administrar contas</string> + <string name="drawer_manage_accounts">Xestionar contas</string> <!-- Simple values --> <string name="simple_boards">Taboleiros</string> @@ -38,6 +38,7 @@ <string name="simple_disabled">desactivado</string> <string name="simple_copied">Copiada</string> <string name="simple_archive">Aquivar</string> + <string name="simple_unassigned">Sen asignar</string> <string name="edit_board">Editar taboleiro</string> <string name="archive_board">Arquivar taboleiro</string> @@ -56,7 +57,7 @@ <string name="about_testers_title">Probadores</string> <string name="about_source_title">Código fonte</string> <string name="about_source">Este proxecto está aloxado en GitHub: %1$s</string> - <string name="about_issues_title">Incidentes</string> + <string name="about_issues_title">Incidencias</string> <string name="about_issues">Vostede pode informar de fallos, propoñer melloras e solicitar funcionalidades na ferramenta de seguimento de erros do GitHub: %1$s</string> <string name="about_translate_title">Traducir</string> <string name="about_translate">Únase ao equipo Nextcloud en Transifex e axúdenos a traducir esta aplicación: %1$s</string> @@ -70,9 +71,6 @@ <string name="about_credits_tab_title">Recoñecementos</string> <string name="about_contribution_tab_title">Colaboración</string> <string name="about_license_tab_title">Licenza</string> - - <string name="copied_to_clipboard">Copiado no portapapeis.</string> - <string name="seconds_ago">hai uns segundos</string> <string name="edit">Editar</string> <string name="label_labels">Seleccionar etiquetas</string> @@ -111,11 +109,12 @@ <string name="account_is_getting_imported">A conta está a importarse</string> <string name="not_synced_yet">Aínda non foi sincronizado</string> <string name="no_lists_yet">Non hai listas</string> - <string name="do_you_want_to_save_your_changes">Confirma que quere gardar os cambios?</string> + <string name="do_you_want_to_save_your_changes">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> + <string name="do_you_want_to_archive_all_cards_of_the_filtered_list">Quere arquivar todas as tarxetas filtradas 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 +128,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> @@ -156,7 +156,6 @@ <string name="maintenance_mode_explanation">O servidor %1$s está atopase en modo de mantemento. Póñase en contacto co seu administrador ou ténteo de novo máis tarde.</string> <string name="share_add_to_card">Engadir á tarxeta</string> <string name="share_success">%1$s foi engadido satisfactoriamente a %2$s</string> - <string name="could_not_copy_to_clipboard">Non foi posíbel copiar ao portapapeis</string> <string name="add_comment">Engadir comentario</string> <string name="card_edit_comments">Comentarios</string> <string name="no_comments_yet">Aínda non hai comentarios</string> @@ -216,7 +215,7 @@ <string name="error_dialog_title">Vaites, e agora que? 🙁</string> <string name="error_dialog_tip_token_mismatch_retry">Probe a forzar o peche da apli e reiniciala de novo. Pode haber unha conexión incorrecta coa apli Nextcloud.</string> - <string name="error_dialog_tip_token_mismatch_clear_storage">Se o problema continúa, tente limpar o almacenamento de ambas as aplis: Nextcloud e Nextcloud Deck para resolver este problema.</string> + <string name="error_dialog_tip_clear_storage_might_help">Se o problema continúa, tente limpar o almacenamento de ambas as aplis: Nextcloud e Nextcloud Deck para resolver este problema.</string> <string name="error_dialog_tip_database_upgrade_failed">Produciuse un fallo ao anovar a base de datos. Informe do problema e borre o almacenamento para usar a apli normalmente.</string> <string name="error_dialog_tip_clear_storage">Pode limpar o almacenamento abrindo a información da apli e seleccionando Almacenamento → Limpar o almacenamento.</string> <string name="error_dialog_tip_files_outdated">A súa apli Nextcloud semella estar desactualizada. Visite Play Store ou F-Droid para obter a última versión.</string> @@ -231,6 +230,8 @@ <string name="error_dialog_we_need_info">Necesitamos a seguinte información técnica para axudarlle:</string> <string name="error_dialog_redirect">O seu servidor respondeu cun código de estado HTTP 302, o que implica que non ten instalada a aplicación Deck no servidor ou que algo está mal configurado. Isto pode ser causado por substitucións personalizadas nun ficheiro .htaccess ou por aplicacións Nextcloud como Client OID.</string> <string name="error_dialog_version_not_parsable">Non foi posíbel determinar a versión da apli Deck do lado do servidor. Asegúrese de que estea instalado e activado.</string> + <string name="error_dialog_account_might_not_be_authorized">É posíbel que a conta da súa apli do Nextcloud xa non estea autorizada.</string> + <string name="error_dialog_user_not_found_in_database">O usuario actual non coincide co que temos na nosa base de datos. Se está a usar LDAP na súa instancia do Nextcloud, é posíbel que a súa apli do Nextcloud teña almacenado un ID de usuario antigo.</string> <string name="error_dialog_capabilities_not_parsable">Non foi posíbel obter as funcionalidades do seu servidor. Asegúrese de que o seu servidor funciona correctamente e que outras aplis de cliente poidan acceder ao Nextcloud.</string> <string name="error_dialog_attachment_upload_failed">Non foi posíbel enviar un arquivo anexo. Probe compartilo doutro xeito e infórmenos deste erro.</string> <string name="error_dialog_tip_disable_battery_optimizations">Desactive todas as optimizacións de batería para Nextcloud e a apli Deck.</string> @@ -243,17 +244,46 @@ <string name="info_box_version_not_supported">A versión %1$s do servidor non é compatíbel, actualice a %2$s</string> <string name="share_link">Compartir ligazón</string> <string name="archive_cards">Arquivar as tarxetas</string> - <string name="manage_accounts">Administrar contas</string> - <string name="manage_list">Administrar a lista</string> + + <string name="manage_accounts">Xestionar contas</string> + <string name="manage_list">Xestionar a lista</string> <string name="simple_reply">Responder</string> <string name="error_while_uploading_attachment">Produciuse un erro ao enviar o anexo: %1$s</string> <string name="append_text_to_description">Anexo á descrición</string> <string name="add_text_as_comment">Engadir como comentario</string> <string name="progress_count">%1$d de %2$d</string> <plurals name="progress_error_count"> - <item quantity="one">%1$d erros ao enviar.</item> - <item quantity="other">%1$d erros ao enviar.</item> + <item quantity="one">%1$d erros ao enviar</item> + <item quantity="other">%1$d erros ao enviar</item> </plurals> <string name="simple_report">Informe</string> <string name="error_action_open_battery_settings">Axustes da batería</string> + <string name="move_warning">Non é posíbel transferir nin comentarios nin ficheiros anexos ao mover a tarxeta a outro taboleiro.</string> + <string name="clone_board">Clonar taboleiro</string> + <string name="cloning_board">Clonando %1$s…</string> + <string name="successfully_cloned_board">%1$s foi clonado satisfactoriamente</string> + <string name="attachment_does_not_yet_exist">Aínda non existe o anexo no Deck</string> + <string name="card_does_not_yet_exist">Aínda non existe a tarxeta no Deck</string> + + <string name="widget_stack_title">Lista</string> + <string name="widget_stack_header_icon">Icona de cabeceira do trebello</string> + <string name="widget_stack_placeholder_icon">Icona de marcador de posición do trebello</string> + <string name="select_stack">Seleccionar a lista</string> + <string name="project_type_deck_board">Taboleiro do Deck</string> + <string name="project_type_deck_card">Tarxeta do Deck</string> + <string name="project_type_file">Ficheiro</string> + <string name="projects_title">Proxectos</string> + <plurals name="resources_count"> + <item quantity="one">%1$d recurso</item> + <item quantity="other">%1$d recursos</item> + </plurals> + <string name="no_assigned_label">Non hai etiqueta asignada</string> + <string name="single_card">Terxeta única</string> + <string name="project_type_room">Sala de conversas</string> + <string name="simple_move">Mover</string> + <string name="cannot_upload_files_without_permission">Non é posíbel enviar ficheiros sen permiso</string> + <string name="clone_cards">Clonar tarxetas</string> + <string name="simple_clone">Clonar</string> + <string name="user_avatar">Avatar do usuario</string> + <string name="simple_unassign">Desasignar</string> </resources> diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index ba71635e7..afd2d349e 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -38,7 +38,6 @@ <string name="simple_disabled">מושבת</string> <string name="simple_copied">הועתק</string> <string name="simple_archive">לארכיון</string> - <string name="edit_board">ערוך לוח</string> <string name="archive_board">לוח פעיל</string> <string name="delete_board">מחק לוח</string> @@ -70,9 +69,6 @@ <string name="about_credits_tab_title">תודות</string> <string name="about_contribution_tab_title">תרומה</string> <string name="about_license_tab_title">רישיון</string> - - <string name="copied_to_clipboard">הועתק ללוח הגזירים</string> - <string name="seconds_ago">לפני מספר שניות</string> <string name="edit">עריכה</string> <string name="label_labels">בחירת תגיות</string> @@ -159,7 +155,6 @@ <string name="maintenance_mode_explanation">השרת %1$s נמצא במצב תחזוקה. נא ליצור קשר עם ההנהלה או לנסות מאוחר יותר.</string> <string name="share_add_to_card">הוספה לכרטיס</string> <string name="share_success">%1$s נוסף בהצלחה אל %2$s</string> - <string name="could_not_copy_to_clipboard">לא ניתן להעתיק ללוח הגזירים</string> <string name="add_comment">הוספת הערה</string> <string name="card_edit_comments">תגובות</string> <string name="no_comments_yet">אין הערות עדיין</string> @@ -216,6 +211,9 @@ <string name="server_error">שגיאת שרת</string> <string name="shared_error">שיתוף תוכן מיישומוני צד־שלישי עדיין לא נתמך לגמרי. נא לנסות להוריד תחילה ואז לשתף ממנהל הקבצים או הגלריה המובנים.</string> <string name="error_dialog_title">שוד ושבר - מה עכשיו? 🙁</string> + <string name="error_dialog_tip_files_outdated">נראה כי יישומון ה־Nextcloud שלך אינו עדכני. נא לבקר בחנות Play או ב־F-Droid כדי לעדכן לגרסה האחרונה.</string> + <string name="error_dialog_tip_files_delete_storage">אם כפיית עצירה לא עוזרת, ניתן לנסות לרוקן את האחסון של שני היישומונים.</string> + <string name="error_dialog_timeout_instance">לא חזרה תגובה מהשרת שלך בזמן קצוב. נא לוודא שהעותק פועל כראוי.</string> <string name="error_dialog_check_maintenance">נא לבדוק שעותק ה־Nextcloud שלך אינו במצב תחזוקה כרגע.</string> <string name="error_dialog_we_need_info">אנו זקוקים לפירוט הטכני הבא כדי לסייע לך:</string> <string name="error_action_open_deck_info">פתיחת פרטי יישומון</string> @@ -229,4 +227,9 @@ <string name="manage_accounts">ניהול חשבונות</string> <string name="simple_reply">תגובה</string> <string name="simple_report">דיווח</string> + <string name="clone_board">שכפול לוח</string> + <string name="widget_stack_title">רשימה</string> + <string name="project_type_file">קובץ</string> + <string name="projects_title">מיזמים</string> + <string name="simple_move">העברה</string> </resources> diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index ed1573282..76875b14d 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -38,6 +38,7 @@ <string name="simple_disabled">onemogućeno</string> <string name="simple_copied">Kopirano</string> <string name="simple_archive">Arhiva</string> + <string name="simple_unassigned">Nedodijeljeno</string> <string name="edit_board">Uredi ploču</string> <string name="archive_board">Arhiviraj ploču</string> @@ -70,9 +71,6 @@ <string name="about_credits_tab_title">Zasluge</string> <string name="about_contribution_tab_title">Doprinos</string> <string name="about_license_tab_title">Licenca</string> - - <string name="copied_to_clipboard">Kopirano u međuspremnik</string> - <string name="seconds_ago">prije nekoliko sekundi</string> <string name="edit">Uredi</string> <string name="label_labels">Odaberite oznake</string> @@ -80,6 +78,8 @@ <string name="hint_assign_people">Dodijeli korisnike</string> <string name="hint_due_date_date">Datum dospijeća</string> <string name="card_not_found">Kartica nije pronadjena</string> + <string name="card_not_found_message">Kartica nije pronađena. Možda je nedavno izbrisana.</string> + <string name="add_account">Dodaj račun</string> <string name="choose_account">Odaberi račun</string> <string name="add_card">Dodaj karticu</string> @@ -110,6 +110,7 @@ <string name="not_synced_yet">Još nije sinkronizirano</string> <string name="no_lists_yet">Još nema popisa</string> <string name="do_you_want_to_save_your_changes">Želite li spremiti promjene?</string> + <string name="do_you_want_to_archive_all_cards_of_the_list">Želite li arhivirati sve kartice iz %1$s?</string> <plurals name="do_you_want_to_delete_the_current_list"> <item quantity="one">Trajno ćete izbrisati %1$d karticu s ovog popisa.</item> <item quantity="few">Trajno ćete izbrisati %1$d kartice s ovog popisa.</item> @@ -128,6 +129,7 @@ <string name="delete_board_message">Trajno ćete izbrisati ovu ploču, uključujući sve stogove i kartice.</string> <string name="settings_theme_title">Tamna tema</string> <string name="settings_branding_title">Brendiranje</string> + <string name="settings_compact_title">Kompaktni način rada</string> <string name="settings_background_sync">Pozadinska sinkronizacija</string> <string name="pref_value_wifi_and_mobile">Sinkronizacija putem bežičnih (Wi-Fi) i mobilnih podataka</string> <string name="pref_value_wifi_only">Sinkroniziraj samo putem bežične (Wi-Fi) mreže</string> @@ -155,7 +157,6 @@ <string name="maintenance_mode_explanation">Poslužitelj %1$s trenutno je u načinu održavanja. Obratite se svom administratoru ili pokušajte kasnije.</string> <string name="share_add_to_card">Dodaj kartici</string> <string name="share_success">%1$s uspješno dodano %2$s</string> - <string name="could_not_copy_to_clipboard">Kopiranje u međuspremnik nije uspjelo</string> <string name="add_comment">Dodaj komentar</string> <string name="card_edit_comments">Komentari</string> <string name="no_comments_yet">Još nema komentara</string> @@ -211,26 +212,77 @@ <string name="server_misconfigured">Pogrešna konfiguracija poslužitelja</string> <string name="server_error">Greška poslužitelja</string> <string name="shared_error">Dijeljenje sadržaja iz aplikacija trećih strana još uvijek nije potpuno podržano. Pokušajte ga preuzeti i zatim dijeliti putem upravitelja podacima ili galerije.</string> + <string name="error_edit_activity_killed_by_android">Android je zatvorio način uređivanja jer treba više resursa sustava za ostale aplikacije.</string> + <string name="error_dialog_title">Ups, što sada? 🙁</string> <string name="error_dialog_tip_token_mismatch_retry">Pokušajte prisilno zatvoriti aplikaciju i ponovo je pokrenuti. Možda se radi o nepravilnoj vezi s aplikacijom Nextcloud.</string> - <string name="error_dialog_tip_token_mismatch_clear_storage">Ako problem i dalje nije otklonjen, pokušajte izbrisati pohranjene podatke iz obje aplikacije: Nextcloud i Nextcloud Deck.</string> + <string name="error_dialog_tip_clear_storage_might_help">Ako problem i dalje nije otklonjen, pokušajte izbrisati pohranjene podatke iz obje aplikacije: Nextcloud i Nextcloud Deck.</string> + <string name="error_dialog_tip_database_upgrade_failed">Nadogradnja baze podataka nije uspjela. Prijavite ovu poteškoću i izbrišite pohranu kako biste se koristili aplikacijom na uobičajeni način.</string> + <string name="error_dialog_tip_clear_storage">Pohranjene podatke možete izbrisati tako da otvorite informacije o aplikaciji i odaberete Pohrana → Brisanje pohrane podataka.</string> <string name="error_dialog_tip_files_outdated">Čini se da je vaša aplikacija Nextcloud zastarjela. Posjetite trgovinu Play Store ili F-Droid kako biste dohvatili najnoviju inačicu.</string> <string name="error_dialog_tip_files_force_stop">Čini se da nešto nije u redu s vašom aplikacijom Nextcloud. Pokušajte prisilno zatvoriti aplikaciju Nextcloud i aplikaciju Nextcloud Deck.</string> <string name="error_dialog_tip_files_delete_storage">Ako prisilno zatvaranje nije otklonilo problem, pokušajte izbrisati pohranjene podatke iz obje aplikacije.</string> <string name="error_dialog_timeout_instance">Nije stigao odgovor poslužitelja u zadanom vremenskom razdoblju. Provjerite radi li vaša instanca.</string> + <string name="error_dialog_timeout_toggle">Provjerite mrežnu vezu. Ponekad može pomoći isključivanje i ponovno uključivanje mobilnih podataka ili bežične (Wi-Fi) mreže.</string> <string name="error_dialog_check_server">Odgovor poslužitelja nije točan. Provjerite možete li aplikaciji Deck pristupiti putem web-sučelja.</string> <string name="error_dialog_check_server_logs">Postoji određeni problem s vašim Nextcloudom. Provjerite datoteke zapisa poslužitelja.</string> <string name="error_dialog_check_maintenance">Provjerite je li vaša instanca Nextclouda trenutno u načinu održavanja.</string> <string name="error_dialog_insufficient_storage">Nema slobodnog prostora za pohranu u vašoj instanci Nextclouda. Izbrišite dio datoteka kako biste sinkronizirali lokalne promjene u oblak.</string> <string name="error_dialog_we_need_info">Potrebne su nam sljedeće tehničke informacije kako bismo vam pomogli:</string> <string name="error_dialog_redirect">Vaš je poslužitelj vratio šifru statusa HTTP 302, što znači da aplikacija Deck nije instalirana na poslužitelju ili postoji pogreška u konfiguraciji. To mogu uzrokovati prilagođena prepisivanja u datoteci .htaccess ili određene aplikacije u Nextcloudu, kao što je OID Client.</string> + <string name="error_dialog_version_not_parsable">Nismo mogli utvrditi inačicu aplikacije Deck na strani poslužitelja. Provjerite je li instalirana i omogućena.</string> + <string name="error_dialog_account_might_not_be_authorized">Račun vaše aplikacije Nextcloud možda više nije ovlašten.</string> + <string name="error_dialog_user_not_found_in_database">Trenutačni korisnik ne odgovara korisniku kojeg imamo u našoj bazi podataka. Ako upotrebljavate LDAP na svojoj instanci Nextclouda, vaša aplikacija Nextcloud možda je pohranila stari ID korisnika.</string> + <string name="error_dialog_capabilities_not_parsable">Nismo uspjeli dohvatiti mogućnosti vašeg poslužitelja. Provjerite radi li vaš poslužitelj pravilno i mogu li druge korisničke aplikacije pristupiti Nextcloudu.</string> + <string name="error_dialog_attachment_upload_failed">Nije moguće otpremiti privitak. Pokušajte ga podijeliti na neki drugi način i javite nam pojedinosti o ovoj pogrešci.</string> + <string name="error_dialog_tip_disable_battery_optimizations">Onemogućite sve optimizacije baterije za aplikacije Nextcloud i Deck.</string> + <string name="error_action_open_deck_info">Otvori informacije o aplikaciji</string> <string name="error_action_open_network">Postavke mreže</string> + <string name="error_action_server_logs">Zapisi poslužitelja</string> <string name="error_action_install">Instaliraj</string> <string name="error_action_report_issue">Prijavi</string> <string name="info_box_maintenance_mode">Poslužitelj u načinu održavanja</string> <string name="info_box_version_not_supported">Inačica poslužitelja %1$s nije podržana, ažurirajte poslužitelj na inačicu %2$s</string> <string name="share_link">Dijeli poveznicu</string> + <string name="archive_cards">Arhiviraj kartice</string> + <string name="manage_accounts">Upravljaj računima</string> + <string name="manage_list">Upravljaj popisom</string> <string name="simple_reply">Odgovori</string> + <string name="error_while_uploading_attachment">Pogreška pri otpremanju privitka: %1$s</string> + <string name="append_text_to_description">Dodaj opisu</string> + <string name="add_text_as_comment">Dodaj kao komentar</string> + <string name="progress_count">%1$d od %2$d</string> + <plurals name="progress_error_count"> + <item quantity="one">%1$d pogreška pri otpremanju</item> + <item quantity="few">%1$d pogreške pri otpremanju</item> + <item quantity="other">%1$d pogreški pri otpremanju</item> + </plurals> <string name="simple_report">Prijavi</string> + <string name="error_action_open_battery_settings">Postavke baterije</string> + <string name="move_warning">Pri premještanju kartice na drugu ploču ne mogu se prenijeti komentari ni privitci.</string> + <string name="clone_board">Kloniraj ploču</string> + <string name="cloning_board">Kloniranje %1$s…</string> + <string name="successfully_cloned_board">Uspješno klonirano %1$s</string> + <string name="attachment_does_not_yet_exist">Privitak ne postoji u aplikaciji Deck</string> + <string name="card_does_not_yet_exist">Kartica ne postoji u aplikaciji Deck</string> + + <string name="widget_stack_title">Popis</string> + <string name="widget_stack_header_icon">Ikona zaglavlja widgeta</string> + <string name="widget_stack_placeholder_icon">Privremena ikona widgeta</string> + <string name="select_stack">Odaberi popis</string> + <string name="project_type_deck_board">Deck ploča</string> + <string name="project_type_deck_card">Deck kartica</string> + <string name="project_type_file">Datoteka</string> + <string name="projects_title">Projekti</string> + <plurals name="resources_count"> + <item quantity="one">%1$d resurs</item> + <item quantity="few">%1$d resursa</item> + <item quantity="other">%1$d resursa</item> + </plurals> + <string name="no_assigned_label">Nema dodijeljenih oznaka</string> + <string name="single_card">Jedna kartica</string> + <string name="project_type_room">Soba za razgovor</string> + <string name="simple_move">Premjesti</string> + <string name="cannot_upload_files_without_permission">Otpremanje datoteka bez dopuštenja nije moguće</string> </resources> diff --git a/app/src/main/res/values-hu-rHU/strings.xml b/app/src/main/res/values-hu-rHU/strings.xml index fc6e5ea13..1b18a371d 100644 --- a/app/src/main/res/values-hu-rHU/strings.xml +++ b/app/src/main/res/values-hu-rHU/strings.xml @@ -38,6 +38,7 @@ <string name="simple_disabled">letiltva</string> <string name="simple_copied">Másolva</string> <string name="simple_archive">Archívum</string> + <string name="simple_unassigned">Nem hozzárendelt</string> <string name="edit_board">Tábla szerkesztése</string> <string name="archive_board">Tábla archiválása</string> @@ -70,9 +71,6 @@ <string name="about_credits_tab_title">Köszönet</string> <string name="about_contribution_tab_title">Közreműködés</string> <string name="about_license_tab_title">Licenc</string> - - <string name="copied_to_clipboard">Vágólapra másolva</string> - <string name="seconds_ago">pár másodperce</string> <string name="edit">Szerkesztés</string> <string name="label_labels">Címkék kiválasztása</string> @@ -112,6 +110,8 @@ <string name="not_synced_yet">Még nincs szinkronizálva</string> <string name="no_lists_yet">Még nincsenek listák</string> <string name="do_you_want_to_save_your_changes">Biztos, hogy menti a változásokat?</string> + <string name="do_you_want_to_archive_all_cards_of_the_list">Biztos, hogy archiválja a(z) %1$s összes kártyáját?</string> + <string name="do_you_want_to_archive_all_cards_of_the_filtered_list">Kívánja a %1$s minden szűrt kártyáját arhiválni?</string> <plurals name="do_you_want_to_delete_the_current_list"> <item quantity="one">Ez véglegesen töröl %1$d kártyát a listából.</item> <item quantity="other">Ez véglegesen törli mind a(z) %1$d kártyát a listából.</item> @@ -128,6 +128,7 @@ <string name="delete_board_message">Ez véglegesen törli ezt a táblát az összes listával és kártyával együtt.</string> <string name="settings_theme_title">Sötét téma</string> <string name="settings_branding_title">Márkázas</string> + <string name="settings_compact_title">Kompakt mód</string> <string name="settings_background_sync">Háttér-szinkronizálás</string> <string name="pref_value_wifi_and_mobile">Szinkronizálás Wi-Fin és mobil adatkapcsolaton</string> <string name="pref_value_wifi_only">Szinkronizálás csak Wi-Fin</string> @@ -155,7 +156,6 @@ <string name="maintenance_mode_explanation">A(z) %1$s kiszolgáló jelenleg karbantartási módban van. Lépjen kapcsolatba a rendszergazdával vagy próbálja újra.</string> <string name="share_add_to_card">Kártyához adás</string> <string name="share_success">%1$s sikeresen hozzáadva ehhez: %2$s</string> - <string name="could_not_copy_to_clipboard">Nem sikerült a vágólapra másolni</string> <string name="add_comment">Hozzászólás hozzáadása</string> <string name="card_edit_comments">Hozzászólások</string> <string name="no_comments_yet">Még nincsenek hozzászólások</string> @@ -215,7 +215,8 @@ <string name="error_dialog_title">Jaj ne – Most mi legyen? 🙁</string> <string name="error_dialog_tip_token_mismatch_retry">Próbálja kényszeríteni az alkalmazás bezárását, és indítsa újra. Lehet, hogy hibás volt a kapcsolat a többi Nextcloud alkalmazással.</string> - <string name="error_dialog_tip_token_mismatch_clear_storage">Ha a probléma továbbra is fennáll, próbálja kiüríteni mindkét alkalmazás tárolóját: a Nextcloudét és a Nextcloud Kártyákét.</string> + <string name="error_dialog_tip_clear_storage_might_help">Ha a probléma továbbra is fennáll, próbálja kiüríteni mindkét alkalmazás tárolóját: a Nextcloudét és a Nextcloud Kártyákét.</string> + <string name="error_dialog_tip_database_upgrade_failed">Az adatbázis frissítése sikertelen. Jelentse a problémát és az alkalmazás normális használatához törölje a tárolót.</string> <string name="error_dialog_tip_clear_storage">A tárolót az alkalmazásinformáció megnyitásával, és a Tárhely → Tárhely ürítése választásával törölheti.</string> <string name="error_dialog_tip_files_outdated">A Nextcloud alkalmazása elavultnak tűnik. Keresse fel a Play Áruházat vagy az F-Droidot, hogy beszerezze a legfrissebb verziót.</string> <string name="error_dialog_tip_files_force_stop">Valami hibásnak tűnik a Nextcloud alkalmazásában. Próbálja mindkettőt leállítani, a Nextcloud és a Nextcloud Kártyák alkalmazást is.</string> @@ -229,7 +230,11 @@ <string name="error_dialog_we_need_info">A következő műszaki információkra van szükségünk, hogy segíthessünk:</string> <string name="error_dialog_redirect">A kiszolgálója 302-es HTTP állapotkóddal válaszolt, amely arra utal, hogy nincs telepítve a Kártyák alkalmazás a kiszolgálón, vagy valami hibásan van beállítva. Ezt egyéni felülírások is okozhatják, mint egy .htaccess-fájl vagy az OID klienshez hasonló Nextcloud alkalmazások.</string> <string name="error_dialog_version_not_parsable">Nem sikerült meghatározni a kiszolgálóoldali Kártyák alkalmazás verzióját. Győződjön meg róla, hogy telepítve és engedélyezve van.</string> + <string name="error_dialog_account_might_not_be_authorized">A Nextcloud alkalmazáshoz tartozó fiókja lehet hogy már lehet, hogy nincs hitelesítve.</string> + <string name="error_dialog_user_not_found_in_database">A jelenlegi felhasználó nem egyezik az adatbázisban tárolttal. Ha LDAP-ot használt a Nextcloud példányán, akkor lehet, hogy a Nextcloud alkalmazás még egy régebbi felhasználóazonosítót tárol.</string> <string name="error_dialog_capabilities_not_parsable">A képességek lekérése a kiszolgálójáról sikertelen. Győződjön meg róla, hogy a kiszolgálója rendesen fut-e, és hogy más kliensalkalmazások elérik-e a Nextcloudod.</string> + <string name="error_dialog_attachment_upload_failed">A melléklet nem tölthető fel. Próbálja meg máshogy megosztani, és tájékoztasson minket a hibáról.</string> + <string name="error_dialog_tip_disable_battery_optimizations">Kapcsoljon ki minden energiatakarékossági beállítást a Nextcloud valamint a Kártyák alkalmazáshoz. </string> <string name="error_action_open_deck_info">Alkalmazásinformációk megnyitása</string> <string name="error_action_open_network">Hálózati beállítások</string> <string name="error_action_server_logs">Kiszolgálónaplók</string> @@ -239,7 +244,46 @@ <string name="info_box_version_not_supported">A kiszolgálóverzió nem támogatott: %1$s, frissítsen erre: %2$s</string> <string name="share_link">Hivatkozás megosztása</string> <string name="archive_cards">Archív kártyák</string> + <string name="manage_accounts">Fiókok kezelése</string> + <string name="manage_list">Lista kezelése</string> <string name="simple_reply">Válasz</string> + <string name="error_while_uploading_attachment">Hiba a melléklet feltöltésekor: %1$s</string> + <string name="append_text_to_description">Hozzáfűzés a leíráshoz</string> + <string name="add_text_as_comment">Hozzáadás megjegyzésként</string> + <string name="progress_count">%1$d / %2$d</string> + <plurals name="progress_error_count"> + <item quantity="one">%1$d hiba történt feltöltéskor</item> + <item quantity="other">%1$d hiba történt feltöltéskor</item> + </plurals> <string name="simple_report">Jelentés</string> - </resources> + <string name="error_action_open_battery_settings">Akkumulátorbeállítások</string> + <string name="move_warning">Sem a megjegyzések, sem a mellékletek nem vihetők át, ha másik táblára helyezi át a kártyát.</string> + <string name="clone_board">Tábla klónozása</string> + <string name="cloning_board">%1$s klónozása…</string> + <string name="successfully_cloned_board">%1$s sikeresen klónozva.</string> + <string name="attachment_does_not_yet_exist">A melléklet még nem létezik a Kártyákban</string> + <string name="card_does_not_yet_exist">A kártya még nem létezik a Kártyákban</string> + + <string name="widget_stack_title">Lista</string> + <string name="widget_stack_header_icon">Modul fejlécikonja</string> + <string name="widget_stack_placeholder_icon">Modul helykitöltő ikonja</string> + <string name="select_stack">Válasszon listát</string> + <string name="project_type_deck_board">Kártyatábla</string> + <string name="project_type_deck_card">Kártya</string> + <string name="project_type_file">Fájl</string> + <string name="projects_title">Projektek</string> + <plurals name="resources_count"> + <item quantity="one">%1$d erőforrás</item> + <item quantity="other">%1$d erőforrás</item> + </plurals> + <string name="no_assigned_label">Nincs címke hozzárendelve</string> + <string name="single_card">Egyetlen kártya</string> + <string name="project_type_room">Beszélgetés szoba</string> + <string name="simple_move">Áthelyezés</string> + <string name="cannot_upload_files_without_permission">Engedély nélkül nem tölthetők fel fájlok</string> + <string name="clone_cards">Kártyák klónozása</string> + <string name="simple_clone">Klónozás</string> + <string name="user_avatar">Felhasználói avatar</string> + <string name="simple_unassign">Elvétel</string> +</resources> diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index d9bde9148..f959b76fe 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -38,6 +38,7 @@ <string name="simple_disabled">disabilitato</string> <string name="simple_copied">Copiato</string> <string name="simple_archive">Archivio</string> + <string name="simple_unassigned">Non assegnato</string> <string name="edit_board">Modifica lavagna</string> <string name="archive_board">Archivia lavagna</string> @@ -70,9 +71,6 @@ <string name="about_credits_tab_title">Riconoscimenti</string> <string name="about_contribution_tab_title">Contributi</string> <string name="about_license_tab_title">Licenza</string> - - <string name="copied_to_clipboard">Copiato negli appunti</string> - <string name="seconds_ago">secondi fa</string> <string name="edit">Modifica</string> <string name="label_labels">Seleziona etichette</string> @@ -113,6 +111,7 @@ <string name="no_lists_yet">Ancora nessun elenco</string> <string name="do_you_want_to_save_your_changes">Vuoi salvare le tue modifiche?</string> <string name="do_you_want_to_archive_all_cards_of_the_list">Vuoi archiviare tutte le schede di %1$s?</string> + <string name="do_you_want_to_archive_all_cards_of_the_filtered_list">Vuoi archiviare tutte le schede filtrate di %1$s?</string> <plurals name="do_you_want_to_delete_the_current_list"> <item quantity="one">Questo eliminerà definitivamente %1$d scheda di questo elenco</item> <item quantity="other">Questo eliminerà definitivamente %1$d schede di questo elenco</item> @@ -129,6 +128,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> @@ -156,7 +156,6 @@ <string name="maintenance_mode_explanation">Il server %1$s attualmente è in manutenzione. Contatta l\'amministratore o riprova più tardi.</string> <string name="share_add_to_card">Aggiungi a scheda</string> <string name="share_success">Aggiunto %1$s a %2$s correttamente</string> - <string name="could_not_copy_to_clipboard">Impossibile copiare negli appunti</string> <string name="add_comment">Aggiungi commento</string> <string name="card_edit_comments">Commenti</string> <string name="no_comments_yet">Ancora nessun commento</string> @@ -216,7 +215,7 @@ <string name="error_dialog_title">Oh no - E adesso? 🙁</string> <string name="error_dialog_tip_token_mismatch_retry">Prova a forzare la chiusura dell\'applicazione e riavviarla nuovamente. Potrebbe essersi verificato un problema di connessione all\'applicazione Nextcloud. </string> - <string name="error_dialog_tip_token_mismatch_clear_storage">Se il problema persiste, prova a cancellare l\'archiviazione di entrambe le applicazioni Nextcloud e Nextcloud Deck per risolvere il problema.</string> + <string name="error_dialog_tip_clear_storage_might_help">Se il problema persiste, prova a cancellare l\'archiviazione di entrambe le applicazioni Nextcloud e Nextcloud Deck per risolvere il problema.</string> <string name="error_dialog_tip_database_upgrade_failed">L\'aggiornamento del database non è riuscito. Segnala il problema e cancella l\'archiviazione per utilizzare l\'applicazione normalmente.</string> <string name="error_dialog_tip_clear_storage">Puoi a cancellare l\'archiviazione aprendo le informazioni dell\'applicazione e selezionando Archiviazione → Cancella archiviazione.</string> <string name="error_dialog_tip_files_outdated">La tua applicazione di Nextcloud sembra essere datata. Visita il Play Store o F-Droid per ottenere l\'ultima versione.</string> @@ -231,6 +230,8 @@ <string name="error_dialog_we_need_info">Ci servono le seguenti informazioni tecniche per aiutarti:</string> <string name="error_dialog_redirect">Il tuo server ha risposto con un codice di stato HTTP 302, che implica che non hai installato l\'applicazione Deck sul tuo server o qualcosa non è configurato correttamente. Questo può essere causato da configurazioni personalizzate nel file .htaccess o da applicazioni di Nextcloud come OID Client.</string> <string name="error_dialog_version_not_parsable">Impossibile determinare la versione dell\'applicazione Deck lato server. Assicurati che sia installata e abilitata.</string> + <string name="error_dialog_account_might_not_be_authorized">L\'account della tua applicazione Nextcloud potrebbe non essere più autorizzato.</string> + <string name="error_dialog_user_not_found_in_database">L\'utente attuale non corrisponde all\'utente presente nel nostro database. Se stai usando LDAP sulla tua istanza di Nextcloud, la tua applicazione potrebbe aver memorizzato un vecchio identificativo utente.</string> <string name="error_dialog_capabilities_not_parsable">Non siamo riusciti a recuperare le funzionalità del tuo server. Assicurati che il server sia in esecuzione e che le altre applicazioni siano in grado di accedere a Nextcloud.</string> <string name="error_dialog_attachment_upload_failed">Un allegato non può essere caricato. Prova a condividerlo in un altro modo e informaci di questo problema.</string> <string name="error_dialog_tip_disable_battery_optimizations">Disabilita tutte le ottimizzazioni della batteria per le applicazioni Nextcloud e Deck.</string> @@ -243,6 +244,7 @@ <string name="info_box_version_not_supported">Versione del server %1$s non supportata, aggiorna a %2$s</string> <string name="share_link">Condividi collegamento</string> <string name="archive_cards">Archivia schede</string> + <string name="manage_accounts">Gestisci account</string> <string name="manage_list">Gestisci elenco</string> <string name="simple_reply">Rispondi</string> @@ -256,4 +258,32 @@ </plurals> <string name="simple_report">Segnala</string> <string name="error_action_open_battery_settings">Impostazioni batteria</string> + <string name="move_warning">Né i commenti, né gli allegati possono essere trasferiti quando si sposta la scheda su un\'altra lavagna.</string> + <string name="clone_board">Clona lavagna</string> + <string name="cloning_board">Clonazione di %1$s…</string> + <string name="successfully_cloned_board">%1$s clonato correttamente</string> + <string name="attachment_does_not_yet_exist">L\'allegato non esiste ancora in Deck</string> + <string name="card_does_not_yet_exist">La scheda non esiste ancora in Deck</string> + + <string name="widget_stack_title">Elenco</string> + <string name="widget_stack_header_icon">Icona intestazione widget</string> + <string name="widget_stack_placeholder_icon">Icona segnaposto widget</string> + <string name="select_stack">Seleziona elenco</string> + <string name="project_type_deck_board">Lavagna di Deck</string> + <string name="project_type_deck_card">Scheda di Deck</string> + <string name="project_type_file">File</string> + <string name="projects_title">Progetti</string> + <plurals name="resources_count"> + <item quantity="one">%1$d risorsa</item> + <item quantity="other">%1$d risorse</item> + </plurals> + <string name="no_assigned_label">Nessuna etichetta assegnata</string> + <string name="single_card">Scheda singola</string> + <string name="project_type_room">Stanza di Talk</string> + <string name="simple_move">Sposta</string> + <string name="cannot_upload_files_without_permission">Impossible caricare file senza permesso</string> + <string name="clone_cards">Clona schede</string> + <string name="simple_clone">Clona</string> + <string name="user_avatar">Avatar dell\'utente</string> + <string name="simple_unassign">Rimuovi assegnazione</string> </resources> diff --git a/app/src/main/res/values-ja-rJP/strings.xml b/app/src/main/res/values-ja-rJP/strings.xml index 7934aa0aa..f66d29f15 100644 --- a/app/src/main/res/values-ja-rJP/strings.xml +++ b/app/src/main/res/values-ja-rJP/strings.xml @@ -38,6 +38,7 @@ <string name="simple_disabled">無効</string> <string name="simple_copied">コピーしました</string> <string name="simple_archive">アーカイブ</string> + <string name="simple_unassigned">未割り当て</string> <string name="edit_board">ボードを編集</string> <string name="archive_board">ボードをアーカイブ</string> @@ -70,9 +71,6 @@ <string name="about_credits_tab_title">クレジット</string> <string name="about_contribution_tab_title">貢献</string> <string name="about_license_tab_title">ライセンス</string> - - <string name="copied_to_clipboard">クリップボードにコピーされました</string> - <string name="seconds_ago">数秒前</string> <string name="edit">編集</string> <string name="label_labels">タグを選択..</string> @@ -127,6 +125,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> @@ -154,7 +153,6 @@ <string name="maintenance_mode_explanation">サーバー%1$sは現在メンテナンスモードです。管理者に問い合わせるか後で試してください。</string> <string name="share_add_to_card">カードに追加</string> <string name="share_success">%1$sを%2$sに追加できました</string> - <string name="could_not_copy_to_clipboard">クリップボードにコピーできませんでした</string> <string name="add_comment">コメントを追加</string> <string name="card_edit_comments">コメント</string> <string name="no_comments_yet">コメントはまだありません</string> @@ -214,7 +212,8 @@ <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 Deck</string> + <string name="error_dialog_tip_clear_storage_might_help">この問題が続くようでしたら、下記両方のアプリでストレージをクリアして問題が解決できるか試してください: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アプリが古いようです。プレイストアかF-Droidから最新版を入手してください。</string> <string name="error_dialog_tip_files_force_stop">Nextcloudアプリで何か不具合が発生しました。NextcloudアプリとNextcloud Deckアプリの両方を強制終了してください。</string> @@ -228,7 +227,12 @@ <string name="error_dialog_we_need_info">サポートには下記技術情報が必要です:</string> <string name="error_dialog_redirect">サーバがHTTPステータスコード 302を返しました。これはDeckアプリがインストールされていないか、構成が誤っていることを意味します。.htaccessファイルのカスタムオーバーライドやOIDクライアントのようなNextcloudアプリが原因の可能性があります。</string> <string name="error_dialog_version_not_parsable">サーバー側のDeckアプリのバージョンを判別できませんでした。Deckアプリがインストール,有効化されていることを確認してください。</string> + <string name="error_dialog_account_might_not_be_authorized">Nextcloudアプリのアカウントが認証されていない可能性があります。</string> + <string name="error_dialog_user_not_found_in_database">現在のユーザーは、データベースに登録されているユーザーと一致しません。NextcloudインスタンスでLDAPを使用している場合、Nextcloudアプリに古いユーザーIDが保存されている可能性があります。</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> @@ -238,8 +242,40 @@ <string name="info_box_version_not_supported">サーバーのバージョン%1$sはサポートしていませんので、%2$sにアップデートしてください。</string> <string name="share_link">URLで共有</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="other">%1$dアップロード中にエラーが発生しました。</item> + </plurals> <string name="simple_report">報告</string> + <string name="error_action_open_battery_settings">バッテリー設定</string> + <string name="move_warning">カードを別のボードに移動する際に、コメントや添付ファイルを転送することはできません。</string> + <string name="clone_board">ボードを複製</string> + <string name="cloning_board">%1$sを複製しています...</string> + <string name="successfully_cloned_board">%1$sの複製に成功しました</string> + <string name="attachment_does_not_yet_exist">添付ファイルはDeckには存在しません</string> + <string name="card_does_not_yet_exist">カードはDeckには存在しません</string> + + <string name="widget_stack_title">リスト</string> + <string name="widget_stack_header_icon">ウィジェットのヘッダーアイコン</string> + <string name="widget_stack_placeholder_icon">ウィジェットの代替アイコン</string> + <string name="select_stack">リストを選択</string> + <string name="project_type_deck_board">Deckボード</string> + <string name="project_type_deck_card">Deckカード</string> + <string name="project_type_file">ファイル</string> + <string name="projects_title">プロジェクト</string> + <plurals name="resources_count"> + <item quantity="other">%1$dリソース</item> + </plurals> + <string name="no_assigned_label">割り当てられたタグはありません</string> + <string name="single_card">1つのカード</string> + <string name="project_type_room">トークルーム</string> + <string name="simple_move">移動</string> + <string name="cannot_upload_files_without_permission">権限が無いためアップロードできません</string> </resources> diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index d45e135d1..2163e68f6 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -38,6 +38,7 @@ <string name="simple_disabled">비활성화</string> <string name="simple_copied">복사됨</string> <string name="simple_archive">보관</string> + <string name="simple_unassigned">할당되지 않음</string> <string name="edit_board">게시판 편집</string> <string name="archive_board">게시판 보관</string> @@ -64,13 +65,12 @@ <string name="about_app_license">이 앱은 GNU General Public License v3+로 배포됩니다.</string> <string name="about_app_license_button">라이선스 보기</string> <string name="about_icons_disclaimer_title">아이콘</string> + <string name="about_icons_disclaimer_app_icon">원래 아이콘을 보고싶으시면 %1$s</string> + <string name="about_icons_disclaimer_mdi_icons">이 후 %1$s앱의 모든 아이콘의 사용은 Google. Inc.가 만들고 Apache 2.0 License아래 보호됩니다.</string> <string name="about_icons_disclaimer_mdi">Material 디자인 아이콘</string> <string name="about_credits_tab_title">크레딧</string> <string name="about_contribution_tab_title">기여자</string> <string name="about_license_tab_title">라이선스</string> - - <string name="copied_to_clipboard">클립보드로 복사</string> - <string name="seconds_ago">몇 초 전</string> <string name="edit">수정</string> <string name="label_labels">태그 선택</string> @@ -110,6 +110,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="other">이 리스트의 모든 %1$d카드가 영구적으로 지워집니다.</item> </plurals> @@ -123,6 +124,8 @@ <string name="deck_outdated_please_update">너무 오래된 버전입니다(%1$s). 업데이트 해주십시오.</string> <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> @@ -150,7 +153,6 @@ <string name="maintenance_mode_explanation">%1$s서버는 현재 유지보수 모드입니다. 관리자에게 연락하거나 나중에 시도해주십시오.</string> <string name="share_add_to_card">카드를 더하십시오.</string> <string name="share_success">%1$s가 %2$s에 성공적으로 더해졌습니다.</string> - <string name="could_not_copy_to_clipboard">클립보드에 복사할 수 없습니다.</string> <string name="add_comment">덧글 달기</string> <string name="card_edit_comments">댓글</string> <string name="no_comments_yet">덧글이 없음</string> @@ -158,6 +160,7 @@ <string name="add_a_new_board_using_the_button">+버튼을 사용해서 새로운 보드를 더하십시오.</string> <string name="choose_board">보드 선택</string> <string name="choose_list">리스트 선택</string> + <string name="task_count">%1$s/%2$s</string> <string name="open_in_browser">브라우저에서 열기</string> <string name="updating_card">카드 업데이트 중...</string> @@ -209,13 +212,15 @@ <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 Deck라는 두 앱을 스토리지에서 지워 이 문제를 해결하십시오.ㅣ</string> + <string name="error_dialog_tip_clear_storage_might_help">이슈가 지속되면 Nextcloud와 Nextcloud Deck라는 두 앱을 스토리지에서 지워 이 문제를 해결하십시오.ㅣ</string> + <string name="error_dialog_tip_database_upgrade_failed">데이터베이스 업그레이드 실패. 오류를 보고하고 정상적인 앱사용을 위해 저장소를 정리하세요.</string> <string name="error_dialog_tip_clear_storage">저장공간을 비우기 위해 app info를 열고 Storage → Clear storage를 선택해주십시오.</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 Deck앱 둘 다 강제종료 해보십시오.</string> <string name="error_dialog_tip_files_delete_storage">강제 종료가 도움이 되지 않는다면, 두 앱 모두의 저장공간을 비워보십시오.</string> <string name="error_dialog_timeout_instance">서버로부터 응답이 없습니다. 당신의 인스턴스가 잘 작동하는지 확인해 주십시오.</string> <string name="error_dialog_timeout_toggle">네트워크 연결을 확인하십시오. 종종 모바일데이터나 와이파이를 껐다가 키는 것이 도움이 될 것입니다.</string> + <string name="error_dialog_check_server">서버의 응답이 틀렸습니다. 덱에 접근할 수 있는지 웹인터페이스로 확인해 주세요.</string> <string name="error_action_open_deck_info">앱 정보 열기</string> <string name="error_action_open_network">네트워크 환경설정</string> <string name="error_action_server_logs">서버 기록</string> @@ -224,7 +229,20 @@ <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="append_text_to_description">설명에 추가</string> + <string name="add_text_as_comment">댓글로 추가하기</string> <string name="simple_report">보고</string> + <string name="error_action_open_battery_settings">배터리 설정</string> + <string name="clone_board">게시판 복제</string> + <string name="cloning_board">%1$s 복제중...</string> + <string name="successfully_cloned_board">%1$s이(가) 성공적으로 복제되었습니다.</string> + <string name="widget_stack_title">목록</string> + <string name="project_type_file">파일</string> + <string name="projects_title">프로젝트</string> + <string name="simple_move">이동</string> </resources> diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml new file mode 100644 index 000000000..57c999618 --- /dev/null +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -0,0 +1,284 @@ +<resources> + <string name="navigation_drawer_open">Åpne navigasjonsskuff</string> + <string name="navigation_drawer_close">Lukk navigasjonsskuff</string> + + <string name="drawer_header_background">Bakgrunsbilde for innledning på meny</string> + <string name="drawer_current_account">Nåværende konto</string> + <string name="drawer_middle_account">Mellomkonto</string> + <string name="drawer_end_account">Siste konto</string> + <string name="drawer_manage_accounts">Håndter kontoer</string> + + <!-- Simple values --> + <string name="simple_boards">Tavler</string> + <string name="simple_add">Legg til</string> + <string name="simple_save">Lagre</string> + <string name="simple_more">Mer</string> + <string name="simple_title">Tittel</string> + <string name="simple_copy">kopier</string> + <string name="simple_synchronization">Synkronisering</string> + <string name="simple_appearance">Utseende</string> + <string name="simple_error">Feil</string> + <string name="simple_exception">Avbrudd</string> + <string name="simple_close">lukk</string> + <string name="simple_open">Åpne</string> + <string name="simple_switch">Endre</string> + <string name="simple_filter">Filter</string> + <string name="simple_overdue">Utløpt</string> + <string name="simple_clear">Tøm</string> + <string name="simple_discard">slett</string> + <string name="simple_update">Oppdater</string> + <string name="simple_delete">Slett</string> + <string name="simple_rename">Endre navn</string> + <string name="simple_settings">Innstillinger</string> + <string name="simple_undo">Angre</string> + <string name="simple_manage">håndtere</string> + <string name="simple_share">del</string> + <string name="simple_select">Velg</string> + <string name="simple_comment">Kommentar</string> + <string name="simple_disabled">deaktiver</string> + <string name="simple_copied">Kopiert</string> + <string name="simple_archive">Arkiv</string> + <string name="simple_unassigned">Ikke tildelt</string> + + <string name="edit_board">Rediger tavle</string> + <string name="archive_board">Arkiver tavle</string> + <string name="delete_board">Slett tavle</string> + <string name="delete_something">Slett %1$s</string> + + <!-- About --> + <string name="about">Om</string> + <string name="about_version_title">Versjon</string> + <string name="about_version">Du bruker forøyeblikket %1$s</string> + <string name="about_maintainer_title">Vedlikeholder</string> + <string name="about_developers_title">Utviklere</string> + <string name="about_translators_title">Oversettere</string> + <string name="about_translators_transifex">Nextcloud-fellesskapet på %1$s</string> + <string name="about_translators_transifex_label">Transifex</string> + <string name="about_testers_title">Testere</string> + <string name="about_source_title">Kildekode</string> + <string name="about_source">Dette prosjektet er på GitHub: %1$s</string> + <string name="about_issues_title">Problemer</string> + <string name="about_issues">Du kan rapportere inn feil, forbedringsforslag og funksjonsforespørsler på GitHub issue tracker: %1$s</string> + <string name="about_translate_title">Oversett</string> + <string name="about_translate">Bli med og hjelp oss å oversette Nextcloud på vår Transifex side: %1$s </string> + <string name="about_app_license_title">App-lisens</string> + <string name="about_app_license">Denne appen er lisensiert under GNU GENERAL PUBLIC LICENSE v3+.</string> + <string name="about_app_license_button">Se lisens</string> + <string name="about_icons_disclaimer_title">Ikoner</string> + <string name="about_icons_disclaimer_app_icon">For original ikon se%1$s.</string> + <string name="about_icons_disclaimer_mdi_icons">Alle øvrige ikoner brukt i denne appen er %1$s laget av Google Inc. og lisensiert under Apache 2.0-lisens.</string> + <string name="about_icons_disclaimer_mdi">Materialdesignikoner</string> + <string name="about_credits_tab_title">Bidragsytere</string> + <string name="about_contribution_tab_title">Bidrag</string> + <string name="about_license_tab_title">Lisens</string> + <string name="seconds_ago">sekunder siden</string> + <string name="edit">Rediger</string> + <string name="label_labels">Velg merkelapper</string> + <string name="label_description">Beskrivelse</string> + <string name="hint_assign_people">Tildel brukere</string> + <string name="hint_due_date_date">Forfallsdato</string> + <string name="card_not_found">Kort ikke funnet</string> + <string name="card_not_found_message">Kortet kunne ikke bli funnet. Mulig det har blitt slettet nylig.</string> + + <string name="add_account">Legg til en konto</string> + <string name="choose_account">Velg konto</string> + <string name="add_card">Legg til nytt kort</string> + <string name="activity">Aktivitet</string> + <string name="add_list">Legg til liste</string> + <string name="rename_list">Gi nytt navn på listen</string> + <string name="delete_list">Slett listen</string> + <string name="label_menu">Meny</string> + <string name="action_card_assign">Tildel til meg</string> + <string name="action_card_unassign">Fjern tildeling til meg</string> + <string name="action_card_archive">Arkiver kort</string> + <string name="action_card_delete">Slett kort</string> + <string name="add_board">Legg til tavle</string> + + <string name="label_clear_due_date">Fjern forfallsdato</string> + <string name="label_add">Legg til%1$s</string> + + <string name="card_edit_details">Detaljer</string> + <string name="card_edit_attachments">Vedlegg</string> + <string name="card_edit_activity">Aktivitet</string> + <string name="about_server_app_version_text">Tjenerversjon:</string> + <string name="no_files_attached_to_this_card">Det er ingen filter vedlagt dette kortet.</string> + <string name="attachments">Vedlegg</string> + <string name="no_cards">Ingen kort enda</string> + <string name="no_account">Ingen konto konfigurert</string> + <string name="account_already_added">Kontoen er allerede lagt til</string> + <string name="account_is_getting_imported">Kontoen blir importert</string> + <string name="not_synced_yet">Ikke synkronisert enda</string> + <string name="no_lists_yet">Ingen lister enda</string> + <string name="do_you_want_to_save_your_changes">Ønsker du å lagre din endringer?</string> + <string name="do_you_want_to_archive_all_cards_of_the_list">Vil du arkivere all kort av %1$s?</string> + <plurals name="do_you_want_to_delete_the_current_list"> + <item quantity="one">Dette vil slette %1$d kortet i listen permanent.</item> + <item quantity="other">Dette vil slette alle %1$d kortene i listen permanent.</item> + </plurals> + <plurals name="do_you_want_to_delete_the_label"> + <item quantity="one">Dette vil fjerne etiketten fra %1$d kortet.</item> + <item quantity="other">Dette vil fjerne etiketten fra %1$d kortene.</item> + </plurals> + <string name="add_a_new_list_using_the_button">Legg til ny liste ved å bruke + knappen</string> + <string name="add_a_new_card_using_the_button">Legg til nytt kort ved å bruke + knappen</string> + <string name="update_deck">Oppdater «deck»</string> + <string name="your_deck_version_is_too_old">Din tjenerversjon av «deck» er updatert</string> + <string name="deck_outdated_please_update">Din tjenerversjon av «deck» er utdatert (%1$s). Vennligst oppdater for å bruke denne android appen som klient.</string> + <string name="delete_board_message">Dette vil uopprettelig slette denne tavlen inkludert alle lister og kort.</string> + <string name="settings_theme_title">Mørkt tema</string> + <string name="settings_branding_title">Branding</string> + <string name="settings_compact_title">Kompakt modus</string> + <string name="settings_background_sync">Synkronisering i bakgrunnen</string> + <string name="pref_value_wifi_and_mobile">Synkroniser med WiFi og mobil data</string> + <string name="pref_value_wifi_only">Synkroniser kun med WiFi</string> + <string name="pref_value_theme_light">Lys</string> + <string name="unassigned_user">Ikke tildelt %1$s</string> + <string name="no_activities">Det er ingen aktiviteter på dette kortet. Du må være koblet til internett for å kunne laste inn og vise aktivitetene.</string> + <string name="share_board">Del tavle</string> + <string name="you_are_currently_offline">Du er frakoblet for øyeblikket</string> + <string name="you_have_to_be_connected_to_the_internet_in_order_to_add_an_account">Du må være koblet til internett for å kunne legge til en konto.</string> + <string name="owner">Eier</string> + <string name="attachment_delete_message">Dette vil slette dette vedlegget permanent.</string> + <string name="no_content">Ikke noe innhold enda</string> + <string name="last_background_sync">Siste bakgrunnssynkronisering:</string> + <string name="simple_off">ett kvarter</string> + <string name="minutes_15">15 minutter</string> + <string name="hour_1">1 time</string> + <string name="hours_6">6 timer</string> + <string name="action_card_move">Flytt kort</string> + <string name="action_card_move_title">Flytt %1$s</string> + <string name="please_add_an_account_first">Vennligst legg til en konto først</string> + <string name="title_is_mandatory">Tittel er obligatorisk</string> + <string name="provide_at_least_a_title_or_description">I det minste gi en tittel eller beskrivelse</string> + <string name="welcome_text">Velkommen til %1$s</string> + <string name="save_card_before_attachment">Kortet må lagres før det kan legges til vedlegg.</string> + <string name="maintenance_mode_explanation">Serveren %1$s er for øyeblikket i vedlikeholds modus. Vennligs kontakt din administrator eller prøv igjen senere.</string> + <string name="share_add_to_card">Legg til i kort</string> + <string name="share_success">%1$sble vellykket lagt til %2$s</string> + <string name="add_comment">Legg til kommentar</string> + <string name="card_edit_comments">Kommentarer</string> + <string name="no_comments_yet">Ingen kommentarer enda</string> + <string name="no_boards">Ingen tavle enda</string> + <string name="add_a_new_board_using_the_button">Legg til ny tavle ved å bruke + knappen</string> + <string name="choose_board">Velg tavle</string> + <string name="choose_list">Velg liste</string> + <string name="task_count">%1$s/%2$s</string> + <string name="open_in_browser">Åpne i nettleser</string> + <string name="updating_card">Oppdaterer kort...</string> + + <!-- Move lists --> + <string name="move_list_right">Flytt listen til høyre</string> + <string name="move_list_left">Flytt listen til venstre</string> + + <!-- Filter --> + <string name="filter_no_filter">Alle</string> + <string name="filter_overdue">Utløpt</string> + <string name="filter_today">I dag</string> + <string name="filter_week">Neste 7 dager</string> + <string name="filter_month">Neste 30 dager</string> + <string name="filter_no_due">Ingen forfallsdato</string> + <string name="filter_by_tag">Filtrer på knagg</string> + <string name="filter_by_assigned_user">Filtrer på tildelt bruker</string> + <string name="filter_by_duedate">Filtrer på forfallsdato</string> + + <!-- Archived cards --> + <string name="archived_cards">Arkiver tavle</string> + <string name="action_card_dearchive">Angre arkivering av kort</string> + <string name="action_archived_cards">Bla gjennom arkiverte kort</string> + <string name="attachment_already_exists">Vedlegget eksisterer allerede</string> + <string name="pick_custom_color">Velg tilpasset farge</string> + <string name="manage_tags">Håndtere etiketter</string> + <string name="add_tag">Legg til merkelapp</string> + <string name="tag_already_exists">%1$s eksisterer allerede</string> + <string name="tag_successfully_added">%1$s </string> + <string name="edit_tag">Rediger %1$s</string> + <string name="filter_tags_title">Merkelapper</string> + <string name="filter_user_title">Brukere</string> + <string name="filter_duedate_title">Forfallsdato</string> + + <!-- Archived boards --> + <string name="action_board_dearchive">Angre arkivering av tavle</string> + <string name="archived_boards">Arkiverte tavler</string> + + <!-- Errors --> + <string name="error">En feil oppstod</string> + <string name="synchronization_failed">Synkronisering mislyktes</string> + <string name="operation_not_yet_supported">Ikke støttet enda</string> + <string name="error_revoking_ac">Feil ved tilbakekalling av tilgang for%1$s</string> + <string name="error_create_label">Feil ved opprettelse av etikett %1$s</string> + <string name="maintenance_mode">Server i vedlikeholdsmodus</string> + <string name="server_misconfigured">Server feilkonfigurert</string> + <string name="server_error">Tjenerfeil</string> + <string name="shared_error">Deling av innhold fra 3dje-partsapper støttes ikke fullt ut. Prøv å laste den ned først og deretter dele den fra din lokale filbehandler eller ditt galleri.</string> + <string name="error_edit_activity_killed_by_android">Android avsluttet redigeringsmodus fordi det trengte flere systemressurser til andre apper.</string> + + <string name="error_dialog_title">Å nei - Hva nå? 🙁</string> + <string name="error_dialog_tip_token_mismatch_retry">Prøv å tvinge avslutning av appen og start den på nytt. Det kan ha vært en feiltilkobling til Nextcloud-appen.</string> + <string name="error_dialog_tip_clear_storage_might_help">Hvis problemet vedvarer, prøv å tømme lageret for begge disse appene for å løse problemet: Nextcloud og Nextcloud Deck.</string> + <string name="error_dialog_tip_database_upgrade_failed">Oppgraderingen av databasen mislyktes. Vennligst rapporter problemet og tøm lageret for å bruke appen normalt.</string> + <string name="error_dialog_tip_clear_storage">Du kan tømme lageret ved å åpne App info og velge Lagring og buffer → Tøm lagring.</string> + <string name="error_dialog_tip_files_outdated">Det virker som Nextcloud appen er utdatert. Vennligst besøk Play Store eller F-Droid for å finne siste versjon.</string> + <string name="error_dialog_tip_files_force_stop">Det virker som noe er galt med Nextcloud appen. Vennligst prøv å tvinge avslutning av Nextcloud appen og Nextcloud Deck appen.</string> + <string name="error_dialog_tip_files_delete_storage">Hvis det ikke hjelper med tving avslutt, så kan du prøve å tømme lageret for begge appene.</string> + <string name="error_dialog_timeout_instance">Serveren svarer ikke innen en gitt tid. Vennligst påse at serveren er tilgjengelig.</string> + <string name="error_dialog_timeout_toggle">Sjekk nettverkstilkoblingen. Noen ganger hjelper det å skru av og på mobil data eller WI-FI tilkoblingen.</string> + <string name="error_dialog_check_server">Svaret fra serveren var feil. Vennligst påse at du har tilgang til Deck appen via nettgrensesnittet.</string> + <string name="error_dialog_check_server_logs">Det er et problem med Nextcloud oppsettet ditt. Vennligst ta en titt i loggfilene til serveren.</string> + <string name="error_dialog_check_maintenance">Vennligst sjekk at Nextcloud instansen ikke er i vedlikeholdsmodus for øyeblikket.</string> + <string name="error_dialog_insufficient_storage">Nextcloud instansen har ikke mer ledig lagringsplass. Vennligst slett noen filer for å synkronisere de lokale endringen til skyen din.</string> + <string name="error_dialog_we_need_info">Vi trenger følgende teknisk informasjon for å kunne hjelpe deg:</string> + <string name="error_dialog_redirect">Serveren din svarte med en HTTP 302-statuskode, noe som antyder at du ikke har installert Deck-appen på serveren din, eller at noe er feilkonfigurert. Dette kan være forårsaket av tilpassede overstyringer i en .htaccess-fil eller av Nextcloud-apper som OID Client.</string> + <string name="error_dialog_version_not_parsable">Vi kunne ikke avgjøre hvilken versjon av Deck-appen på serveren. Vennligst forsikre deg om at den er installert og aktivert.</string> + <string name="error_dialog_account_might_not_be_authorized">Kontoen til Nextcloud-appen din er kanskje ikke lenger autorisert.</string> + <string name="error_dialog_user_not_found_in_database">Den nåværende brukeren samsvarer ikke med brukeren vi har i databasen vår. Hvis du bruker LDAP i Nextcloud-instansen, kan det hende at Nextcloud-appen din har lagret en gammel bruker-ID.</string> + <string name="error_dialog_capabilities_not_parsable">Vi kunne ikke hente funksjonene til serveren din. Forsikre deg om at serveren din kjører bra, og at andre klientapper har tilgang til Nextcloud.</string> + <string name="error_dialog_attachment_upload_failed">Et vedlegg kunne ikke lastes opp. Prøv å dele den på en annen måte og gi oss beskjed om denne feilen.</string> + <string name="error_dialog_tip_disable_battery_optimizations">Deaktiver alle batterioptimaliseringer for Nextcloud og Deck-appen.</string> + <string name="error_action_open_deck_info">Åpne App info</string> + <string name="error_action_open_network">Nettverksinnstillinger</string> + <string name="error_action_server_logs">Serverlogger</string> + <string name="error_action_install">Installer</string> + <string name="error_action_report_issue">Rapporter</string> + <string name="info_box_maintenance_mode">Server i vedlikeholdsmodus</string> + <string name="info_box_version_not_supported">Server versjon %1$ser ikke støttet, vennligst oppgrader til %2$s</string> + <string name="share_link">Share link</string> + <string name="archive_cards">Arkiver kortene</string> + + <string name="manage_accounts">Håndter kontoer</string> + <string name="manage_list">Håndtere liste</string> + <string name="simple_reply">Svar</string> + <string name="error_while_uploading_attachment">Feil ved opplasting av vedlegg: %1$s</string> + <string name="append_text_to_description">Legg til beskrivelsen</string> + <string name="add_text_as_comment">Legg til som kommentar</string> + <string name="progress_count">%1$d av %2$d</string> + <plurals name="progress_error_count"> + <item quantity="one">%1$d feil ved opplasting</item> + <item quantity="other">%1$d feil under opplasting</item> + </plurals> + <string name="simple_report">Rapporter</string> + <string name="error_action_open_battery_settings">Batteri innstillinger</string> + <string name="move_warning">Verken kommentarer eller vedlegg kan overføres når du flytter kortet til en annen tavle.</string> + <string name="clone_board">Klon tavle</string> + <string name="cloning_board">Kloner %1$s...</string> + <string name="successfully_cloned_board">Kloning av %1$s var vellykket</string> + <string name="attachment_does_not_yet_exist">Vedlegget finnes ikke i Deck enda</string> + <string name="card_does_not_yet_exist">Kortet finnes ikke i Deck enda</string> + + <string name="widget_stack_title">Liste</string> + <string name="widget_stack_header_icon">Widget topptekstikon</string> + <string name="widget_stack_placeholder_icon">Widget plassholderikon</string> + <string name="select_stack">Velg liste</string> + <string name="project_type_deck_board">Deck tavle</string> + <string name="project_type_deck_card">Deck kort</string> + <string name="project_type_file">Fil</string> + <string name="projects_title">Prosjekter</string> + <plurals name="resources_count"> + <item quantity="one">%1$d ressurs</item> + <item quantity="other">%1$dressurser</item> + </plurals> + <string name="no_assigned_label">Ingen etikett tildelt</string> + <string name="single_card">Enkelt kort</string> + <string name="project_type_room">Talk rom</string> + <string name="simple_move">Flytt</string> + <string name="cannot_upload_files_without_permission">Kan ikke laste opp filer uten tillatelse</string> + </resources> diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 92ba83726..975ef99de 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -38,6 +38,7 @@ <string name="simple_disabled">uitgeschakeld</string> <string name="simple_copied">Gekopieerd</string> <string name="simple_archive">Archiveer</string> + <string name="simple_unassigned">Niet-toegewezen</string> <string name="edit_board">Pas bord aan</string> <string name="archive_board">Archiveer bord</string> @@ -70,9 +71,6 @@ <string name="about_credits_tab_title">Credits</string> <string name="about_contribution_tab_title">Bijdrage</string> <string name="about_license_tab_title">Licentie</string> - - <string name="copied_to_clipboard">Gekopieerd naar het klembord</string> - <string name="seconds_ago">seconden geleden</string> <string name="edit">Bewerken</string> <string name="label_labels">Kies tags</string> @@ -80,7 +78,7 @@ <string name="hint_assign_people">Toewijzen gebruikers</string> <string name="hint_due_date_date">Vervaldatum</string> <string name="card_not_found">Kaart niet gevonden</string> - <string name="card_not_found_message">De kaart kon niet gevonden worden. Misschien is deze recent verwijderd.</string> + <string name="card_not_found_message">De kaart kon niet worden gevonden. Misschien is die recent verwijderd.</string> <string name="add_account">Account toevoegen</string> <string name="choose_account">Kies account</string> @@ -91,7 +89,7 @@ <string name="delete_list">Stapel verwijderen</string> <string name="label_menu">menu</string> <string name="action_card_assign">Wijs kaart toe aan mij</string> - <string name="action_card_unassign">Kaart van mij afstoten</string> + <string name="action_card_unassign">Kaart van mij terughalen</string> <string name="action_card_archive">Archiveer kaart</string> <string name="action_card_delete">Verwijderen kaart</string> <string name="add_board">Toevoegen bord</string> @@ -106,12 +104,13 @@ <string name="no_files_attached_to_this_card">Er zijn geen bestanden gekoppeld aan deze kaart.</string> <string name="attachments">Bijlagen</string> <string name="no_cards">Nog geen kaarten</string> - <string name="no_account">Geen account geconfigureerd.</string> - <string name="account_already_added">Account al toegevoegd.</string> + <string name="no_account">Geen account geconfigureerd</string> + <string name="account_already_added">Account al toegevoegd</string> <string name="account_is_getting_imported">Account wordt geïmporteerd</string> <string name="not_synced_yet">Nog niet gesynchroniseerd</string> - <string name="no_lists_yet">Nog geen stapels</string> + <string name="no_lists_yet">Nog geen lijsten</string> <string name="do_you_want_to_save_your_changes">Wil je de wijzigingen opslaan?</string> + <string name="do_you_want_to_archive_all_cards_of_the_list">Wil je alle kaarten van %1$s archiveren?</string> <plurals name="do_you_want_to_delete_the_current_list"> <item quantity="one">Dit zal deze %1$dkaart permanent van de lijst verwijderen.</item> <item quantity="other">Dit zal alle %1$dkaarten definitief uit de stapel verwijderen.</item> @@ -127,7 +126,8 @@ <string name="deck_outdated_please_update">Je deck versie is te oud (%1$s). Update deze android app om de deck te kunnen gebruiken.</string> <string name="delete_board_message">Dit zal dit bord inclusief alle stapels en kaarten definitief verwijderen.</string> <string name="settings_theme_title">Donker thema</string> - <string name="settings_branding_title">Brandmerken</string> + <string name="settings_branding_title">Branding</string> + <string name="settings_compact_title">Compacte modus</string> <string name="settings_background_sync">Achtergrond synchronisatie</string> <string name="pref_value_wifi_and_mobile">Sync met Wi-Fi en mobiele data</string> <string name="pref_value_wifi_only">Sync alleen met Wi-Fi</string> @@ -152,10 +152,9 @@ <string name="provide_at_least_a_title_or_description">Geef ten minste een titel of beschrijving op</string> <string name="welcome_text">Welkom bij %1$s</string> <string name="save_card_before_attachment">De kaart moet worden bewaard voordat er bijlagen aan kunnen worden toegevoegd.</string> - <string name="maintenance_mode_explanation">De server %1$s is momenteel in onderhoud modus. Neem a.u.b. contact op met de beheerder of probeer het later opnieuw</string> + <string name="maintenance_mode_explanation">De server %1$s is momenteel in onderhoud modus. Neem alstublieft contact op met de beheerder of probeer het later opnieuw</string> <string name="share_add_to_card">Toevoegen aan kaart</string> <string name="share_success">Succesvol %1$s toegevoegd aan %2$s</string> - <string name="could_not_copy_to_clipboard">Kon niet kopiëren naar het klembord</string> <string name="add_comment">Reactie toevoegen</string> <string name="card_edit_comments">Reacties</string> <string name="no_comments_yet">Nog geen reacties</string> @@ -210,26 +209,31 @@ <string name="maintenance_mode">Server in onderhoudsmodus</string> <string name="server_misconfigured">Server verkeerd geconfigureerd</string> <string name="server_error">Serverfout</string> - <string name="shared_error">Het delen van content vanaf 3e-partij apps wordt nog niet volledig ondersteund. Probeer jhet eerst te downloaden en deel dan vanaf je eigen bestandsbeheerder of galerij.</string> + <string name="shared_error">Het delen van content vanaf 3e-partij apps wordt nog niet volledig ondersteund. Probeer het eerst te downloaden en deel dan vanaf je eigen bestandsbeheerder of galerij.</string> <string name="error_edit_activity_killed_by_android">Android beëindigde de bewerkingsmodus omdat het meer systeembronnen nodig had voor andere apps.</string> <string name="error_dialog_title">Oh nee - wat nu? 🙁</string> <string name="error_dialog_tip_token_mismatch_retry">Probeer de app te sluiten en opnieuw te starten. Er was misschien een onjuiste verbinding met de Nextcloud-app.</string> - <string name="error_dialog_tip_token_mismatch_clear_storage">Als het probleem blijft aanhouden, probeer dan de gegevensopslag van beide apps te wissen: zowel Nextcloud als Nextcloud Deck.</string> + <string name="error_dialog_tip_clear_storage_might_help">Als het probleem blijft aanhouden, probeer dan de gegevensopslag van beide apps te wissen: zowel Nextcloud als Nextcloud Deck.</string> + <string name="error_dialog_tip_database_upgrade_failed">Het upgraden van de database is mislukt. Gelieve het probleem te melden en opslagruimte vrij te maken om de app naar behoren te kunnen gebruiken.</string> <string name="error_dialog_tip_clear_storage">Je kan de opslag leegmaken door in Android de app info te openen en kies dan Opslag → Gegevens verwijderen.</string> <string name="error_dialog_tip_files_outdated">Je Nextcloud app lijkt te zijn verouderd. Download alstublieft de nieuwste versie in de Google Play Store of in F-Droid.</string> <string name="error_dialog_tip_files_force_stop">Er lijkt iets mis te zijn met je Nextcloud app. Probeer alstublieft beide apps geforceerd te sluiten, zowel de Nextcloud app als de Nextcloud Deck app.</string> <string name="error_dialog_tip_files_delete_storage">Als geforceerd afsluiten niet helpt, probeer dan van beide apps de gegevensopslag te wissen.</string> <string name="error_dialog_timeout_instance">Er was binnen de gestelde tijd geen reactie ontvangen van je server. Controleer alstublieft of je Nextcloud-instantie wel goed loopt.</string> <string name="error_dialog_timeout_toggle">Controleer je netwerk verbinding. Het kan ook helpen de dataverbinding of WIFI uit en weer aan te zetten.</string> - <string name="error_dialog_check_server">Je server gaf een onjuiste reactie. Controleer alstublieft of de Deck app in de web-interface wel goed werkt.</string> + <string name="error_dialog_check_server">Je server gaf een onjuiste reactie. Controleer alstublieft of je toegang krijgt tot de Deck app via de web-interface.</string> <string name="error_dialog_check_server_logs">Er is iets mis met je Nextcloud instellingen. Controleer alstublieft de server log bestanden.</string> <string name="error_dialog_check_maintenance">Controleer alstublieft of je Nextcloud instantie momenteel niet in onderhouds-modus draait.</string> <string name="error_dialog_insufficient_storage">Je Nextcloud instantie heeft geen vrije ruimte meer. Verwijder alstublieft enkele bestanden om je lokale gegevens te kunnen synchroniseren.</string> <string name="error_dialog_we_need_info">We hebben de volgende technische informatie nodig om je te kunnen helpen: </string> <string name="error_dialog_redirect">Je server heeft gereageerd met een HTTP 302-statuscode, wat inhoudt dat je de Deck-app niet op je server hebt geïnstalleerd of dat er iets verkeerd is geconfigureerd. Dit kan worden veroorzaakt door maatwerk aanpassingen in een .htaccess-bestand of door Nextcloud-apps zoals de OID Client.</string> <string name="error_dialog_version_not_parsable">We konden de versie van de Deck-app aan de serverzijde niet bepalen. Zorg ervoor dat de app is geïnstalleerd en ingeschakeld.</string> + <string name="error_dialog_account_might_not_be_authorized">Het account van uw Nextcloud-app is mogelijk niet langer geautoriseerd.</string> + <string name="error_dialog_user_not_found_in_database">De huidige gebruiker komt niet overeen met de gebruiker die we in onze database hebben. Als u LDAP gebruikt op uw Nextcloud-instantie, heeft uw Nextcloud-app mogelijk een oude gebruikers-ID opgeslagen.</string> <string name="error_dialog_capabilities_not_parsable">We kunnen de mogelijkheden van je server niet vaststellen. Zorg ervoor dat je server goed werkt en dat andere client-apps toegang hebben tot Nextcloud.</string> + <string name="error_dialog_attachment_upload_failed">Een bijlage kon niet worden opgeladen. Gelieve het op een andere manier te delen en ons in te lichten over deze fout.</string> + <string name="error_dialog_tip_disable_battery_optimizations">Gelieve de batterijoptimalisatie uit te schakelen voor de Nextcloud en Deck applicatie.</string> <string name="error_action_open_deck_info">Open App informatie</string> <string name="error_action_open_network">Netwerk instellingen</string> <string name="error_action_server_logs">Server logbestanden</string> @@ -238,7 +242,43 @@ <string name="info_box_maintenance_mode">Server in onderhoudsmodus</string> <string name="info_box_version_not_supported">Server versie %1$s is niet ondersteund, werk bij naar %2$s</string> <string name="share_link">Delen link</string> + <string name="archive_cards">Archiveer kaarten</string> + <string name="manage_accounts">Accounts beheren </string> + <string name="manage_list">Beheer lijst</string> <string name="simple_reply">Antwoord</string> + <string name="error_while_uploading_attachment">Fout tijdens het opladen van bijlage: %1$s</string> + <string name="append_text_to_description">Toevoegen aan beschrijving</string> + <string name="add_text_as_comment">Toevoegen als opmerking</string> + <string name="progress_count">%1$d op %2$d</string> + <plurals name="progress_error_count"> + <item quantity="one">%1$d fouten tijdens het uploaden</item> + <item quantity="other">%1$d fouten tijdens het uploaden</item> + </plurals> <string name="simple_report">Rapporteer</string> + <string name="error_action_open_battery_settings">Batterij instellingen</string> + <string name="move_warning">Bij het verplaatsen van de kaart naar een ander bord kunnen geen opmerkingen of bijlagen worden overgebracht.</string> + <string name="clone_board">Dupliceer bord</string> + <string name="cloning_board">Klonen %1$s ...</string> + <string name="successfully_cloned_board">Succesvol gekloond %1$s</string> + <string name="attachment_does_not_yet_exist">Bijlage bestaat nog niet in Deck</string> + <string name="card_does_not_yet_exist">Kaart bestaat nog niet in Deck</string> + + <string name="widget_stack_title">Lijst</string> + <string name="widget_stack_header_icon">Widget koptekst icoon</string> + <string name="widget_stack_placeholder_icon">Widget placeholder icoon</string> + <string name="select_stack">Lijst selecteren</string> + <string name="project_type_deck_board">Deck board</string> + <string name="project_type_deck_card">Deck kaart</string> + <string name="project_type_file">Bestand</string> + <string name="projects_title">Projecten</string> + <plurals name="resources_count"> + <item quantity="one">%1$d middelen</item> + <item quantity="other">%1$d middelen</item> + </plurals> + <string name="no_assigned_label">Geen toegewezen label</string> + <string name="single_card">Enkele kaart</string> + <string name="project_type_room">Praatruimte</string> + <string name="simple_move">Verplaatsen</string> + <string name="cannot_upload_files_without_permission">Kan geen bestanden uploaden zonder toestemming</string> </resources> diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 4fd76266f..057d6e8d3 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -38,6 +38,7 @@ <string name="simple_disabled">wyłączony</string> <string name="simple_copied">Skopiowano</string> <string name="simple_archive">Archiwizuj</string> + <string name="simple_unassigned">Nieprzypisane</string> <string name="edit_board">Edytuj tablicę</string> <string name="archive_board">Zarchiwizuj tablicę</string> @@ -70,9 +71,6 @@ <string name="about_credits_tab_title">Autorzy</string> <string name="about_contribution_tab_title">Wkład</string> <string name="about_license_tab_title">Licencja</string> - - <string name="copied_to_clipboard">Skopiowano do schowka</string> - <string name="seconds_ago">przed chwilą</string> <string name="edit">Edytuj</string> <string name="label_labels">Wybierz etykietę</string> @@ -113,6 +111,7 @@ <string name="no_lists_yet">Brak list</string> <string name="do_you_want_to_save_your_changes">Czy chcesz zachować zmiany?</string> <string name="do_you_want_to_archive_all_cards_of_the_list">Czy chcesz zarchiwizować wszystkie karty %1$s?</string> + <string name="do_you_want_to_archive_all_cards_of_the_filtered_list">Czy chcesz zarchiwizować wszystkie odfiltrowane karty z %1$s?</string> <plurals name="do_you_want_to_delete_the_current_list"> <item quantity="one">Spowoduje to trwałe usunięcie %1$d karty z tej listy.</item> <item quantity="few">Spowoduje to trwałe usunięcie wszystkich %1$d kart z tej listy.</item> @@ -133,6 +132,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> @@ -160,7 +160,6 @@ <string name="maintenance_mode_explanation">Serwer %1$s jest obecnie w trybie konserwacji. Skontaktuj się z administratorem lub spróbuj ponownie później.</string> <string name="share_add_to_card">Dodaj do karty</string> <string name="share_success">Pomyślnie dodano %1$s do %2$s</string> - <string name="could_not_copy_to_clipboard">Nie można skopiować do schowka</string> <string name="add_comment">Dodaj komentarz</string> <string name="card_edit_comments">Komentarze</string> <string name="no_comments_yet">Brak komentarzy</string> @@ -183,7 +182,7 @@ <string name="filter_week">Następne 7 dni</string> <string name="filter_month">Następne 30 dni</string> <string name="filter_no_due">Brak daty ważności</string> - <string name="filter_by_tag">Filtruj po tagu</string> + <string name="filter_by_tag">Filtruj według etykiety</string> <string name="filter_by_assigned_user">Filtruj według przypisanego użytkownika</string> <string name="filter_by_duedate">Filtruj według terminu</string> @@ -220,7 +219,8 @@ <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_clear_storage_might_help">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> @@ -234,6 +234,8 @@ <string name="error_dialog_we_need_info">Potrzebujemy następujących informacji technicznych, aby Ci pomóc:</string> <string name="error_dialog_redirect">Twój serwer odpowiedział kodem stanu HTTP 302, co oznacza, że nie masz zainstalowanej aplikacji Deck na serwerze lub coś jest źle skonfigurowane. Może to być spowodowane niestandardowymi przekierowaniami pliku .htaccess lub aplikacjami Nextcloud, takimi jak OID Client.</string> <string name="error_dialog_version_not_parsable">Nie udało się ustalić wersji aplikacji Deck po stronie serwera. Upewnij się, że jest zainstalowany i włączony.</string> + <string name="error_dialog_account_might_not_be_authorized">Konto Twojej aplikacji Nextcloud może nie być już autoryzowane.</string> + <string name="error_dialog_user_not_found_in_database">Bieżący użytkownik nie jest zgodny z użytkownikiem, którego mamy w naszej bazie danych. Jeśli używasz protokołu LDAP w swojej instancji Nextcloud, to Twoja aplikacja Nextcloud może przechowywać stary identyfikator użytkownika.</string> <string name="error_dialog_capabilities_not_parsable">Nie udało się sprawdzić możliwości Twojego serwera. Upewnij się, że serwer działa poprawnie oraz czy inne aplikacje klienckie mają dostęp do Nextcloud.</string> <string name="error_dialog_attachment_upload_failed">Nie można wysłać załącznika. Spróbuj udostępnić go w inny sposób oraz powiadom nas o tym błędzie.</string> <string name="error_dialog_tip_disable_battery_optimizations">Wyłącz wszystkie optymalizacje baterii dla aplikacji Nextcloud i aplikacji Deck.</string> @@ -246,6 +248,7 @@ <string name="info_box_version_not_supported">Wersja serwera %1$s nie jest wspierana, zaktualizuj do wersji %2$s</string> <string name="share_link">Udostępnij link</string> <string name="archive_cards">Zarchiwizuj karty</string> + <string name="manage_accounts">Zarządzaj kontami</string> <string name="manage_list">Zarządzaj listą</string> <string name="simple_reply">Odpowiedz</string> @@ -254,11 +257,41 @@ <string name="add_text_as_comment">Dodaj jako komentarz</string> <string name="progress_count">%1$d z %2$d</string> <plurals name="progress_error_count"> - <item quantity="one">%1$d błąd podczas wysyłania.</item> - <item quantity="few">%1$d błędy podczas wysyłania.</item> - <item quantity="many">%1$d błędów podczas wysyłania.</item> - <item quantity="other">%1$d błędów podczas wysyłania.</item> + <item quantity="one">%1$d błąd podczas wysyłania</item> + <item quantity="few">%1$d błędy podczas wysyłania</item> + <item quantity="many">%1$d błędów podczas wysyłania</item> + <item quantity="other">%1$d błędów podczas wysyłania</item> </plurals> <string name="simple_report">Raport</string> <string name="error_action_open_battery_settings">Ustawienia baterii</string> + <string name="move_warning">Nie można przenosić komentarzy, ani załączników karty na inną tablicę.</string> + <string name="clone_board">Klonuj tablicę</string> + <string name="cloning_board">Klonowanie %1$s…</string> + <string name="successfully_cloned_board">Pomyślnie sklonowano %1$s</string> + <string name="attachment_does_not_yet_exist">Załącznik nie istnieje jeszcze w Deck</string> + <string name="card_does_not_yet_exist">Karta nie istnieje jeszcze w Deck</string> + + <string name="widget_stack_title">Lista</string> + <string name="widget_stack_header_icon">Ikona nagłówka widżetu</string> + <string name="widget_stack_placeholder_icon">Ikona zastępcza widżetu</string> + <string name="select_stack">Wybierz listę</string> + <string name="project_type_deck_board">Tablica Deck</string> + <string name="project_type_deck_card">Karta Deck</string> + <string name="project_type_file">Plik</string> + <string name="projects_title">Projektowanie</string> + <plurals name="resources_count"> + <item quantity="one">%1$d zasób</item> + <item quantity="few">%1$d zasoby</item> + <item quantity="many">%1$d zasobów</item> + <item quantity="other">%1$d zasobów</item> + </plurals> + <string name="no_assigned_label">Brak przypisanej etykiety</string> + <string name="single_card">Pojedyncza karta</string> + <string name="project_type_room">Pokój rozmów</string> + <string name="simple_move">Przenieś</string> + <string name="cannot_upload_files_without_permission">Nie można wysyłać plików bez uprawnienia</string> + <string name="clone_cards">Klonuj karty</string> + <string name="simple_clone">Klonuj</string> + <string name="user_avatar">Awatar użytkownika</string> + <string name="simple_unassign">Cofnij przypisanie</string> </resources> diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 8eb21ff3b..9cf3f4f6d 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -38,6 +38,7 @@ <string name="simple_disabled">desativado</string> <string name="simple_copied">Copiado</string> <string name="simple_archive">Arquivados</string> + <string name="simple_unassigned">Não atribuído</string> <string name="edit_board">Editar painel</string> <string name="archive_board">Arquivar painel</string> @@ -70,9 +71,6 @@ <string name="about_credits_tab_title">Créditos</string> <string name="about_contribution_tab_title">Contribuição</string> <string name="about_license_tab_title">Licença</string> - - <string name="copied_to_clipboard">Copiado para a área de transferência</string> - <string name="seconds_ago">segundos atrás</string> <string name="edit">Editar</string> <string name="label_labels">Selecionar etiquetas</string> @@ -113,6 +111,7 @@ <string name="no_lists_yet">Ainda sem listas</string> <string name="do_you_want_to_save_your_changes">Deseja salvar suas alterações?</string> <string name="do_you_want_to_archive_all_cards_of_the_list">Deseja arquivar todos os cartões de %1$s?</string> + <string name="do_you_want_to_archive_all_cards_of_the_filtered_list">Você deseja arquivar todos os cartões filtrados de %1$s?</string> <plurals name="do_you_want_to_delete_the_current_list"> <item quantity="one">Isto excluirá permanentemente %1$d cartão desta lista.</item> <item quantity="other">Isto excluirá permanentemente todos os %1$d cartões desta lista.</item> @@ -129,6 +128,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> @@ -156,7 +156,6 @@ <string name="maintenance_mode_explanation">O servidor %1$s está no modo de manutenção. Entre em contato com o administrador ou tente novamente mais tarde.</string> <string name="share_add_to_card">Adicionar ao cartão</string> <string name="share_success">%1$s adicionado com sucesso à %2$s</string> - <string name="could_not_copy_to_clipboard">Não foi possível copiar para a área de transferência</string> <string name="add_comment">Adicionar comentário</string> <string name="card_edit_comments">Comentários</string> <string name="no_comments_yet">Nenhum comentário ainda</string> @@ -216,7 +215,7 @@ <string name="error_dialog_title">Oh não - E agora? 🙁</string> <string name="error_dialog_tip_token_mismatch_retry">Tente forçar o fechamento do aplicativo e reinicie-o novamente. Pode ter havido uma conexão incorreta com o aplicativo Nextcloud.</string> - <string name="error_dialog_tip_token_mismatch_clear_storage">Se o problema persistir, tente limpar o armazenamento dos dois aplicativos: Nextcloud e Nextcloud Deck para resolver esse problema.</string> + <string name="error_dialog_tip_clear_storage_might_help">Se o problema persistir, tente limpar o armazenamento dos dois aplicativos: Nextcloud e Nextcloud Deck para resolver esse problema.</string> <string name="error_dialog_tip_database_upgrade_failed">A atualização do banco de dados falhou. Relate o problema e limpe o armazenamento para usar o aplicativo normalmente.</string> <string name="error_dialog_tip_clear_storage">Você pode limpar o armazenamento abrindo as informações do aplicativo e selecionando Armazenamento → Limpar Armazenamento</string> <string name="error_dialog_tip_files_outdated">Seu aplicativo Nextcloud parece estar desatualizado. Visite a Play Store ou o F-Droid para obter a versão mais recente.</string> @@ -231,6 +230,8 @@ <string name="error_dialog_we_need_info">Precisamos das seguintes informações técnicas para ajudá-lo:</string> <string name="error_dialog_redirect">Seu servidor respondeu com um código de status HTTP 302, o que implica que você não instalou o aplicativo Deck no servidor ou que algo está configurado incorretamente. Isso pode ser causado por alterações no arquivo .htaccess ou por aplicativos Nextcloud como o Client OID.</string> <string name="error_dialog_version_not_parsable">Não conseguimos determinar a versão do servidor do aplicativo Deck. Confirme se ele está instalado e ativo.</string> + <string name="error_dialog_account_might_not_be_authorized">A conta do seu aplicativo Nextcloud pode não estar mais autorizada.</string> + <string name="error_dialog_user_not_found_in_database">O usuário atual não corresponde ao usuário que temos em nosso banco de dados. Se você estiver usando LDAP na sua instância do Nextcloud, seu aplicativo Nextcloud pode ter armazenado um ID do usuário antigo.</string> <string name="error_dialog_capabilities_not_parsable">Não foi possível buscar os recursos do seu servidor. Verifique se o servidor está funcionando bem e se outros aplicativos clientes podem acessar o Nextcloud.</string> <string name="error_dialog_attachment_upload_failed">Não foi possível carregar um anexo. Tente compartilhá-lo de outra maneira e informe-nos sobre esse bug.</string> <string name="error_dialog_tip_disable_battery_optimizations">Desative todas as otimizações de bateria para o Nextcloud e o aplicativo Deck.</string> @@ -243,6 +244,7 @@ <string name="info_box_version_not_supported">A versão %1$s do servidor não é suportada, atualize para %2$s</string> <string name="share_link">Link de compartilhamento</string> <string name="archive_cards">Cartões arquivados</string> + <string name="manage_accounts">Gerenciar contas</string> <string name="manage_list">Gerenciar lista</string> <string name="simple_reply">Responder</string> @@ -251,9 +253,37 @@ <string name="add_text_as_comment">Adicionar como comentário</string> <string name="progress_count">%1$d de %2$d</string> <plurals name="progress_error_count"> - <item quantity="one">%1$d erros durante o envio.</item> - <item quantity="other">%1$d erros durante o envio.</item> + <item quantity="one">%1$d erro ao fazer o upload</item> + <item quantity="other">%1$d erros ao fazer o upload</item> </plurals> <string name="simple_report">Reportar</string> <string name="error_action_open_battery_settings">Configurações da bateria</string> + <string name="move_warning">Comentários ou anexos não podem ser transferidos ao mover o cartão para outra placa.</string> + <string name="clone_board">Clonar painel</string> + <string name="cloning_board">Clonando %1$s…</string> + <string name="successfully_cloned_board">%1$s clonado com sucesso</string> + <string name="attachment_does_not_yet_exist">O anexo ainda não existe no Deck</string> + <string name="card_does_not_yet_exist">O cartão ainda não existe no Deck</string> + + <string name="widget_stack_title">Lista</string> + <string name="widget_stack_header_icon">Ícone do cabeçalho do widget</string> + <string name="widget_stack_placeholder_icon">Ícone de marcador de posição do widget</string> + <string name="select_stack">Selecionar lista</string> + <string name="project_type_deck_board">Painel de deck</string> + <string name="project_type_deck_card">Cartão de deck</string> + <string name="project_type_file">Arquivo</string> + <string name="projects_title">Projetos</string> + <plurals name="resources_count"> + <item quantity="one">%1$d recurso</item> + <item quantity="other">%1$d recursos</item> + </plurals> + <string name="no_assigned_label">Nenhum rótulo atribuído</string> + <string name="single_card">Cartão único</string> + <string name="project_type_room">Sala de conversação</string> + <string name="simple_move">Mover</string> + <string name="cannot_upload_files_without_permission">Não é possível enviar arquivos sem permissão</string> + <string name="clone_cards">Clonar cartões</string> + <string name="simple_clone">Clonar</string> + <string name="user_avatar">Avatar do usuário</string> + <string name="simple_unassign">Desatribuir</string> </resources> diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index bb95d8196..d2491bc65 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -24,7 +24,7 @@ <string name="simple_switch">Переключить</string> <string name="simple_filter">Фильтр</string> <string name="simple_overdue">Просроченные</string> - <string name="simple_clear">Отчистить</string> + <string name="simple_clear">Очистить</string> <string name="simple_discard">Отменить</string> <string name="simple_update">Обновить</string> <string name="simple_delete">Удалить</string> @@ -34,10 +34,11 @@ <string name="simple_manage">Управлять</string> <string name="simple_share">общий ресурс</string> <string name="simple_select">Выбрать</string> - <string name="simple_comment">Коментарий</string> + <string name="simple_comment">Комментарий</string> <string name="simple_disabled">отключено</string> <string name="simple_copied">Скопировано</string> <string name="simple_archive">Переместить в архив</string> + <string name="simple_unassigned">не назначен</string> <string name="edit_board">Редактировать доску</string> <string name="archive_board">Переместить доску в архив</string> @@ -70,9 +71,6 @@ <string name="about_credits_tab_title">Сообщество</string> <string name="about_contribution_tab_title">Участие</string> <string name="about_license_tab_title">Лицензия</string> - - <string name="copied_to_clipboard">Скопировано в буфер</string> - <string name="seconds_ago">менее минуты</string> <string name="edit">Править</string> <string name="label_labels">Выбор меток</string> @@ -133,6 +131,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> @@ -160,7 +159,6 @@ <string name="maintenance_mode_explanation">Сервер %1$s в режиме обслуживания. Свяжитесь с вашим администратором или повторите попытку позже.</string> <string name="share_add_to_card">Добавить в карточку</string> <string name="share_success">%1$s добавлено в %2$s</string> - <string name="could_not_copy_to_clipboard">Не удалось скопировать в буфер обмена.</string> <string name="add_comment">Добавить комментарий</string> <string name="card_edit_comments">Комментарии</string> <string name="no_comments_yet">Пока никто не прокомментировал</string> @@ -220,8 +218,7 @@ <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_might_help">Если проблема повторяется, попробуйте для её решения очистить хранилище приложений Nextcloud и Nextcloud Карточки.</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> @@ -247,6 +244,7 @@ <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> @@ -254,12 +252,29 @@ <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="many">Во время передачи произошло %1$d ошибок.</item> - <item quantity="other">Во время передачи произошло %1$d ошибки.</item> - </plurals> <string name="simple_report">Сообщить</string> <string name="error_action_open_battery_settings">Настройки аккумулятора</string> -</resources> + <string name="move_warning">нет ни одного комментария или вложения, которые можно перенести при перемещении карточки на другую доску</string> + <string name="clone_board">Скопировать доску</string> + <string name="cloning_board">Копирование %1$s...</string> + <string name="successfully_cloned_board">Успешно скопировано%1$s</string> + <string name="attachment_does_not_yet_exist">Вложение еще не создано в карточке</string> + <string name="card_does_not_yet_exist">Карточка не создана на доске</string> + + <string name="widget_stack_title">Список</string> + <string name="widget_stack_header_icon">иконка заголовка виджета</string> + <string name="select_stack">Выберите список</string> + <string name="project_type_deck_board">Доска</string> + <string name="project_type_deck_card">Карточка</string> + <string name="project_type_file">Файл</string> + <string name="projects_title">Проекты</string> + <plurals name="resources_count"> + <item quantity="one">%1$d ресурс</item> + <item quantity="few">%1$d ресурса</item> + <item quantity="many">%1$d ресурсов</item> + <item quantity="other">%1$d ресурса</item> + </plurals> + <string name="no_assigned_label">Метки не присвоены</string> + <string name="single_card">Отдельная карточка</string> + <string name="simple_move">Переместить</string> + </resources> diff --git a/app/src/main/res/values-sk-rSK/strings.xml b/app/src/main/res/values-sk-rSK/strings.xml index 0898dfb00..62d41c57a 100644 --- a/app/src/main/res/values-sk-rSK/strings.xml +++ b/app/src/main/res/values-sk-rSK/strings.xml @@ -38,6 +38,7 @@ <string name="simple_disabled">zakázané</string> <string name="simple_copied">Skopírované</string> <string name="simple_archive">Archivovať</string> + <string name="simple_unassigned">Nepriradený</string> <string name="edit_board">Upraviť nástenku</string> <string name="archive_board">Archivovať nástenku</string> @@ -70,9 +71,6 @@ <string name="about_credits_tab_title">Zásluhy</string> <string name="about_contribution_tab_title">Príspevok</string> <string name="about_license_tab_title">Licencia</string> - - <string name="copied_to_clipboard">Skopírované do schránky</string> - <string name="seconds_ago">pred niekoľkými sekundami</string> <string name="edit">Upraviť</string> <string name="label_labels">Vybrať štítky</string> @@ -133,6 +131,7 @@ <string name="delete_board_message">Táto nástenka sa natrvalo odstráni, vrátane všetkých zoznamov a kariet.</string> <string name="settings_theme_title">Tmavý motív</string> <string name="settings_branding_title">Použitie vlastného loga</string> + <string name="settings_compact_title">Kompaktný režim</string> <string name="settings_background_sync">Synchronizácia na pozadí</string> <string name="pref_value_wifi_and_mobile">Synchronizovať na Wi-Fi a mobilných dátach</string> <string name="pref_value_wifi_only">Synchronizovať len na Wi-Fi</string> @@ -160,7 +159,6 @@ <string name="maintenance_mode_explanation">Server %1$s je v móde údržby. Kontaktujte svojho správcu alebo to skúste znova neskôr.</string> <string name="share_add_to_card">Pridať do košíka</string> <string name="share_success">%1$s úspešne pridané do %2$s</string> - <string name="could_not_copy_to_clipboard">Nepodarilo sa skopírovať do schránky</string> <string name="add_comment">Pridať komentár</string> <string name="card_edit_comments">Komentáre</string> <string name="no_comments_yet">Zatiaľ žiadne komentáre</string> @@ -220,7 +218,7 @@ <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_clear_storage_might_help">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> @@ -235,6 +233,8 @@ <string name="error_dialog_we_need_info">Aby sme vám pomohli, potrebujeme následovné technické informácie:</string> <string name="error_dialog_redirect">Váš server odpovedal stavovým kódom HTTP 302, čo znamená, že na svojom serveri nemáte nainštalovanú aplikáciu Deck alebo je niečo nesprávne nastavené. Môže to byť spôsobené vlastnými zmenami v súbore .htaccess alebo aplikáciami Nextcloud, ako je OID Client.</string> <string name="error_dialog_version_not_parsable">Nepodarilo sa určiť verziu aplikácie Deck na strane servera. Skontrolujte, či je nainštalovaný a povolený.</string> + <string name="error_dialog_account_might_not_be_authorized">Účet vašej aplikácie Nextcloud už nemusí byť autorizovaný.</string> + <string name="error_dialog_user_not_found_in_database">Aktuálny používateľ sa nezhoduje s používateľom, ktorý máme v našej databáze. Ak používate server LDAP vo vašej inštancii Nextcloud, vaša aplikácia Nextcloud pravdepodobne uložila staré ID používateľa.</string> <string name="error_dialog_capabilities_not_parsable">Nepodarilo sa načítať možnosti vášho servera. Uistite sa, že váš server funguje správne a že ďalšie klientske aplikácie majú prístup k serveru Nextcloud.</string> <string name="error_dialog_attachment_upload_failed">Prílohu nebolo možné nahrať. Skúste to zdieľať iným spôsobom a dajte nám vedieť o tejto chybe.</string> <string name="error_dialog_tip_disable_battery_optimizations">Vypnite všetky optimalizácie batérie pre aplikácie Nextcloud a Deck.</string> @@ -247,6 +247,7 @@ <string name="info_box_version_not_supported">Verzia servera %1$s nie je podporovaná, aktualizujte ju na %2$s</string> <string name="share_link">Sprístupniť odkaz</string> <string name="archive_cards">Archivovať karty</string> + <string name="manage_accounts">Spravovať účty</string> <string name="manage_list">Spravovať zoznam</string> <string name="simple_reply">Odpoveď</string> @@ -255,11 +256,37 @@ <string name="add_text_as_comment">Pridať ako komentár</string> <string name="progress_count">%1$d z %2$d</string> <plurals name="progress_error_count"> - <item quantity="one">%1$d chyba pri nahrávaní</item> - <item quantity="few">%1$d chyby pri nahrávaní</item> - <item quantity="many">%1$d chyby pri nahrávaní</item> - <item quantity="other">%1$d chýb pri nahrávaní</item> + <item quantity="one">%1$d chyba počas nahrávania</item> + <item quantity="few">%1$d chyby počas nahrávania</item> + <item quantity="many">%1$d chyby počas nahrávania</item> + <item quantity="other">%1$d chýb počas nahrávania</item> </plurals> <string name="simple_report">Hlásenie</string> <string name="error_action_open_battery_settings">Nastavenia batérie.</string> -</resources> + <string name="move_warning">Pri presúvaní karty na inú nástenku nie je možné prenášať komentáre ani prílohy.</string> + <string name="clone_board">Duplikovať nástenku</string> + <string name="cloning_board">Klonovanie %1$s…</string> + <string name="successfully_cloned_board">%1$s úspešne klonovaný</string> + <string name="attachment_does_not_yet_exist">Príloha ešte na nástenke neexistuje</string> + <string name="card_does_not_yet_exist">Karta ešte na nástenke neexistuje</string> + + <string name="widget_stack_title">Zoznam</string> + <string name="widget_stack_header_icon">Ikona hlavičky miniaplikácie</string> + <string name="widget_stack_placeholder_icon">Ikona zástupného symbolu miniaplikácie</string> + <string name="select_stack">Vyberte zoznam</string> + <string name="project_type_deck_board">Palubná doska</string> + <string name="project_type_deck_card">Palubná karta</string> + <string name="project_type_file">Súbor</string> + <string name="projects_title">Projekty</string> + <plurals name="resources_count"> + <item quantity="one">%1$d zdroj</item> + <item quantity="few">%1$d zdroje</item> + <item quantity="many">%1$d zdroje</item> + <item quantity="other">%1$d zdrojov</item> + </plurals> + <string name="no_assigned_label">Žiadna priradená značka</string> + <string name="single_card">Jedna karta</string> + <string name="project_type_room">Diskusná miestnosť</string> + <string name="simple_move">Presunúť</string> + <string name="cannot_upload_files_without_permission">Nie je možné nahrávať súbory bez povolenia</string> + </resources> diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index 84239bb54..0e3aa0be8 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -38,6 +38,7 @@ <string name="simple_disabled">onemogočeno</string> <string name="simple_copied">Kopirano!</string> <string name="simple_archive">Arhiv</string> + <string name="simple_unassigned">Nedodeljeno</string> <string name="edit_board">Uredi zbirko</string> <string name="archive_board">Arhiviraj zbirko</string> @@ -70,9 +71,6 @@ <string name="about_credits_tab_title">Zasluge</string> <string name="about_contribution_tab_title">Skupnost</string> <string name="about_license_tab_title">Dovoljenje</string> - - <string name="copied_to_clipboard">Kopirano v odložišče</string> - <string name="seconds_ago">pred nekaj sekundami</string> <string name="edit">Uredi</string> <string name="label_labels">Izbor oznak</string> @@ -113,6 +111,7 @@ <string name="no_lists_yet">Ni še dodanega seznama</string> <string name="do_you_want_to_save_your_changes">Ali želite shraniti spremembe?</string> <string name="do_you_want_to_archive_all_cards_of_the_list">Ali želite arhivirati vse naloge %1$s?</string> + <string name="do_you_want_to_archive_all_cards_of_the_filtered_list">Ali želite arhivirati vse filtrirane naloge %1$s?</string> <plurals name="do_you_want_to_delete_the_current_list"> <item quantity="one">S tem boste trajno izbrisali %1$d nalogo tega seznama.</item> <item quantity="two">S tem boste trajno izbrisali %1$d nalogi tega seznama.</item> @@ -133,6 +132,7 @@ <string name="delete_board_message">S tem dejanjem bo zbirka z vsemi paketi nalog in nalogami trajno izbrisana.</string> <string name="settings_theme_title">Temna tema</string> <string name="settings_branding_title">Prilagajanje oblikovanja</string> + <string name="settings_compact_title">Stisnjen pogled</string> <string name="settings_background_sync"> Usklajevanje v ozadju</string> <string name="pref_value_wifi_and_mobile">Usklajuj na Wi-Fi in mobilni povezavi</string> <string name="pref_value_wifi_only">Usklajuj le na Wi-Fi</string> @@ -160,7 +160,6 @@ <string name="maintenance_mode_explanation">Strežnik %1$s je trenutno v načinu vzdrževanja. Stopite v stik s skrbnikom, ali pa poskusite kasneje.</string> <string name="share_add_to_card">Dodaj k nalogi</string> <string name="share_success">Uspešno dodano %1$s v %2$s</string> - <string name="could_not_copy_to_clipboard">Predmeta ni mogoče kopirati v odložišče</string> <string name="add_comment">Dodaj opombo</string> <string name="card_edit_comments">Opombe</string> <string name="no_comments_yet">Ni še dodane nobene opombe</string> @@ -220,7 +219,8 @@ <string name="error_dialog_title">Ojoj, kaj pa zdaj? 🙁</string> <string name="error_dialog_tip_token_mismatch_retry">Poskusite vsiliti končanje programa in ga ponovno zagnati. Morda je težava v neustrezni povezavi z okoljem Nextcloud</string> - <string name="error_dialog_tip_token_mismatch_clear_storage">Če se težava še naprej pojavlja, poskusite počistiti podatke programa Nextcloud in Nextcloud Deck.</string> + <string name="error_dialog_tip_clear_storage_might_help">Če se težava še naprej pojavlja, poskusite počistiti podatke programa Nextcloud in Nextcloud Deck.</string> + <string name="error_dialog_tip_database_upgrade_failed">Nadgradnja podatkovne zbirke je spodletela. Pošljite poročilo o napaki in nato počistite shrambo za normalno uporabo programa.</string> <string name="error_dialog_tip_clear_storage">Shrambo je mogoče počistiti med podrobnostmi programa Shramba → Počisti shrambo.</string> <string name="error_dialog_tip_files_outdated">Program Nextcloud je očitno starejše različice. Posodobite jo prek Trgovine Play ali s posodabljalnikom F-Droid.</string> <string name="error_dialog_tip_files_force_stop">Kaže, da je nekaj narobe s programom Nextcloud. Poskusite vsiljeno zaustaviti programa Nextcloud in Nextcloud Deck.</string> @@ -234,8 +234,11 @@ <string name="error_dialog_we_need_info">Z ustrezno pomoč potrebujemo nekaj tehničnih podrobnosti sistema:</string> <string name="error_dialog_redirect">S strežnika je prejet odziv s kodo stanja HTTP 302, kar kaže na to, da program Deck ni nameščen, ali pa je napačno nastavljen. Napaka se pojavi tudi pri nekaterih določilih v datoteki .htaccess oziroma programih, kot je odjemalec OID.</string> <string name="error_dialog_version_not_parsable">Ni mogoče določiti različice strežnika, povezanega s programom Deck. Prepričajte se, da je nameščen in omogočen.</string> + <string name="error_dialog_account_might_not_be_authorized">Račun v programu Nextcloud morda ni več overjen.</string> + <string name="error_dialog_user_not_found_in_database">Trenutno izbrani uporabnik nima ustreznih dovoljenj. Če uporabljate LDAP ima morda progam Nextcloud shranjen star ID uporabnika.</string> <string name="error_dialog_capabilities_not_parsable">Ni mogoče pridobiti podatkov o zmožnostih strežnika. Prepričajte se, da strežnik teče in da imajo programi dostop do okolja Nextcloud.</string> <string name="error_dialog_attachment_upload_failed">Priloge ni mogoče poslati. Souporabo omogočite na nek drug način, nam pa posredujte poročilo o hrošču.</string> + <string name="error_dialog_tip_disable_battery_optimizations">Onemogočite vso optimizacijo porabe baterije za programa Nextcloud in Deck.</string> <string name="error_action_open_deck_info">Odpri podrobnosti programa</string> <string name="error_action_open_network">Nastavitve omrežja</string> <string name="error_action_server_logs">Dnevniki strežnika</string> @@ -245,6 +248,7 @@ <string name="info_box_version_not_supported">Različica strežnika %1$s ni podprta. Priporočljiva je posodobitev na različico %2$s.</string> <string name="share_link">Povezava za souporabo</string> <string name="archive_cards">Arhiviraj naloge</string> + <string name="manage_accounts">Upravljanje z računi</string> <string name="manage_list">Upravljanje seznama</string> <string name="simple_reply">Odgovori</string> @@ -259,4 +263,35 @@ <item quantity="other">%1$d napak med pošiljanjem</item> </plurals> <string name="simple_report">Poročilo</string> - </resources> + <string name="error_action_open_battery_settings">Nastavitve baterije</string> + <string name="move_warning">Ni mogoče prenesti ne opomb ne prilog med premikanjem naloge v drugo zbirko.</string> + <string name="clone_board">Kloniraj zbirko</string> + <string name="cloning_board">Poteka kloniranje %1$s…</string> + <string name="successfully_cloned_board">Kloniranje %1$s je uspešno končano</string> + <string name="attachment_does_not_yet_exist">Priloga v Deck še ne obstaja</string> + <string name="card_does_not_yet_exist">Naloga v Deck še ne obstaja</string> + + <string name="widget_stack_title">Seznam</string> + <string name="widget_stack_header_icon">Ikona glave gradnika</string> + <string name="widget_stack_placeholder_icon">Ikona ročnika gradnika</string> + <string name="select_stack">Izbor seznama</string> + <string name="project_type_deck_board">Zbirka Deck</string> + <string name="project_type_deck_card">Naloga Deck</string> + <string name="project_type_file">Datoteka</string> + <string name="projects_title">Projekti</string> + <plurals name="resources_count"> + <item quantity="one">%1$d vir</item> + <item quantity="two">%1$d vira</item> + <item quantity="few">%1$d viri</item> + <item quantity="other">%1$d virov</item> + </plurals> + <string name="no_assigned_label">Ni dodeljene oznake</string> + <string name="single_card">Enojna naloga</string> + <string name="project_type_room">Klepetalnica</string> + <string name="simple_move">Premakni</string> + <string name="cannot_upload_files_without_permission">Brez ustreznih dovoljenj ni mogoče naložiti datoteke</string> + <string name="clone_cards">Kloniraj nalogo</string> + <string name="simple_clone">Kloniraj</string> + <string name="user_avatar">Podoba uporabnika</string> + <string name="simple_unassign">Odstrani</string> +</resources> diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index ad6075d5b..223e72c32 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -38,6 +38,7 @@ <string name="simple_disabled">искључено</string> <string name="simple_copied">Копирано</string> <string name="simple_archive">Архива</string> + <string name="simple_unassigned">Недодељен</string> <string name="edit_board">Измени таблу</string> <string name="archive_board">Архивирај таблу</string> @@ -70,9 +71,6 @@ <string name="about_credits_tab_title">Заслуге</string> <string name="about_contribution_tab_title">Допринос</string> <string name="about_license_tab_title">Лиценца</string> - - <string name="copied_to_clipboard">Копирано у клипборд</string> - <string name="seconds_ago">пре неколико секунди</string> <string name="edit">Измени</string> <string name="label_labels">Одаберите ознаке</string> @@ -112,6 +110,8 @@ <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> + <string name="do_you_want_to_archive_all_cards_of_the_filtered_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> @@ -124,12 +124,13 @@ </plurals> <string name="add_a_new_list_using_the_button">Додај нови списак користећи + дугме</string> <string name="add_a_new_card_using_the_button">Додајте нову картицу на + дугме</string> - <string name="update_deck">Ажурирај шпил</string> + <string name="update_deck">Ажурирај Шпил</string> <string name="your_deck_version_is_too_old">Ваша верзија Шпила је превише стара</string> <string name="deck_outdated_please_update">Верзија апликације Шпил је стара (%1$s). Ажурирајте да бисте користили ову Андроид апликацију као клијента.</string> <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">Синхронизуј и на бежичној и на мобилној вези</string> <string name="pref_value_wifi_only">Синхронизуј само преко бежичне везе</string> @@ -157,7 +158,6 @@ <string name="maintenance_mode_explanation">Сервер %1$s је тренутно у режиму одржавања. Контактирајте администратора или покушајте касније.</string> <string name="share_add_to_card">Додај на картицу</string> <string name="share_success">Успешно додат %1$s на %2$s</string> - <string name="could_not_copy_to_clipboard">Не могу да копирам у оставу</string> <string name="add_comment">Додај коментар</string> <string name="card_edit_comments">Коментари</string> <string name="no_comments_yet">Још нема коментара</string> @@ -217,7 +217,8 @@ <string name="error_dialog_title">О, не - шта сад? 🙁</string> <string name="error_dialog_tip_token_mismatch_retry">Покушајте да форсирано затворите апликацију и стартујете је поново. Можда је била неисправна конекција ка Некстклауд апликацији.</string> - <string name="error_dialog_tip_token_mismatch_clear_storage">Ако проблем настави да се дешава, пробајте да очистите податке од обе апликације: и Некстклауд и Некстклауд Шпила да бисте решили проблем.</string> + <string name="error_dialog_tip_clear_storage_might_help">Ако проблем настави да се дешава, пробајте да очистите податке од обе апликације: и Некстклауд и Некстклауд Шпила да бисте решили проблем.</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">Изгледа да је верзија Некстклауд апликације стара. Посетите Play Store или Ф-дроид да скинете најновију верзију.</string> <string name="error_dialog_tip_files_force_stop">Нешто није у реду да Вашом Некстклауд апликацијом. Пробајте да форсирано зауставите обе, и Некстклауд апликацију и апликацију Некстклауд Шпила.</string> @@ -231,7 +232,11 @@ <string name="error_dialog_we_need_info">Потребне су нам следеће техничке информација да бисмо Вам помогли:</string> <string name="error_dialog_redirect">Сервер је одговорио са HTTP 302 кодом, што значи да немате инсталирану Deck апликацију или да Вам сервер није правилно конфигурисан. Ово може да буде проузорковано произвољним додавањима у .htaccess фајлу или другим Некстклауд апликацијама као што је OID Client.</string> <string name="error_dialog_version_not_parsable">Не можемо да одредимо серверску верзију апликације Шпила. Проверите да ли је инсталирана и укључена.</string> + <string name="error_dialog_account_might_not_be_authorized">Ваш налог од Некстклауд апликације можда више није ауторизован.</string> + <string name="error_dialog_user_not_found_in_database">Тренутни корисник се не поклапа са корисником у нашој бази података. Ако користите LDAP у тренутној Некстклауд сесији, вероватно Вам је Некстклауд апликација сачувала стари кориснички ИД.</string> <string name="error_dialog_capabilities_not_parsable">Не можемо да дохватимо могућности Вашег сервера. Проверите да Вам сервер ради добро и да ли друге клијентске апликације могу да му приступе.</string> + <string name="error_dialog_attachment_upload_failed">Прилог није могао бити отпремљен. Молимо пробајте да га поделите на други начин и пријавите нам ову грешку.</string> + <string name="error_dialog_tip_disable_battery_optimizations">Искључите све оптимизације батерије за Некстаклауд и Шпил апликације.</string> <string name="error_action_open_deck_info">Отвори инфо о апликацији</string> <string name="error_action_open_network">Поставке мреже</string> <string name="error_action_server_logs">Дневник са сервера</string> @@ -240,7 +245,49 @@ <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> + <string name="move_warning">Ни коментари ни прилози не могу да се пребаце када се картица помера на другу таблу.</string> + <string name="clone_board">Клонирај таблу</string> + <string name="cloning_board">Клонирам %1$s…</string> + <string name="successfully_cloned_board">Успешно клониран %1$s</string> + <string name="attachment_does_not_yet_exist">Прилог још не постоји у Шпил апликацији</string> + <string name="card_does_not_yet_exist">Картица још не постоји у Шпил апликацији</string> + + <string name="widget_stack_title">Излистај</string> + <string name="widget_stack_header_icon">Заглавње иконице виџета</string> + <string name="widget_stack_placeholder_icon">Иконица виџета</string> + <string name="select_stack">Одабир списка</string> + <string name="project_type_deck_board">Табла Шпила</string> + <string name="project_type_deck_card">Картица Шпила</string> + <string name="project_type_file">Фајл</string> + <string name="projects_title">Пројекти</string> + <plurals name="resources_count"> + <item quantity="one">%1$d ресурс</item> + <item quantity="few">%1$d ресурси</item> + <item quantity="other">%1$d ресурси</item> + </plurals> + <string name="no_assigned_label">Нема додељене ознаке</string> + <string name="single_card">Засебна картица</string> + <string name="project_type_room">Причаоница</string> + <string name="simple_move">Помери</string> + <string name="cannot_upload_files_without_permission">Не могу да отпремим фајлове без дозвола</string> + <string name="clone_cards">Клонирај картице</string> + <string name="simple_clone">Клонирај</string> + <string name="user_avatar">Кориснички аватар</string> + <string name="simple_unassign">Уклони доделу</string> +</resources> diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 11c7b3500..82d5a3636 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -38,6 +38,7 @@ <string name="simple_disabled">inaktiverad</string> <string name="simple_copied">Kopierad</string> <string name="simple_archive">Arkivera</string> + <string name="simple_unassigned">Ej tilldelad</string> <string name="edit_board">Ändra tavla</string> <string name="archive_board">Arkivera tavla</string> @@ -70,9 +71,6 @@ <string name="about_credits_tab_title">Tack</string> <string name="about_contribution_tab_title">Bidrag</string> <string name="about_license_tab_title">Licens</string> - - <string name="copied_to_clipboard">Kopierat till urklipp</string> - <string name="seconds_ago">sekunder sedan</string> <string name="edit">Redigera</string> <string name="label_labels">Välj taggar</string> @@ -129,6 +127,7 @@ <string name="delete_board_message">Detta kommer att permanent radera denna tavla inklusive alla listor och kort.</string> <string name="settings_theme_title">Mörkt tema</string> <string name="settings_branding_title">Varumärke</string> + <string name="settings_compact_title">Kompakt läge</string> <string name="settings_background_sync">Bakgrundssynkronisering</string> <string name="pref_value_wifi_and_mobile">Synkronisera med Wi-Fi och mobildata</string> <string name="pref_value_wifi_only">Synkronisera endast med Wi-Fi</string> @@ -156,7 +155,6 @@ <string name="maintenance_mode_explanation">Servern %1$s är för närvarande i underhållsläge. Kontakta din administratör eller försök igen senare.</string> <string name="share_add_to_card">Lägg till i kort</string> <string name="share_success">La till %1$s till %2$s</string> - <string name="could_not_copy_to_clipboard">Kunde inte kopiera till urklipp</string> <string name="add_comment">Lägg till kommentar</string> <string name="card_edit_comments">Kommentarer</string> <string name="no_comments_yet">Inga kommentarer än</string> @@ -212,8 +210,26 @@ <string name="server_misconfigured">Server felkonfigurerad</string> <string name="server_error">Serverfel</string> <string name="shared_error">Delning av innehåll från appar från tredje part stöds ännu inte helt. Vänligen försök att hämta det först och dela det sedan från din ursprungliga filhanterare eller galleri.</string> + <string name="error_edit_activity_killed_by_android">Android avslutade redigeringsläget eftersom det behövde mer systemresurser för andra appar.</string> + <string name="error_dialog_title">Å nej - Vad händer nu?🙁</string> <string name="error_dialog_tip_token_mismatch_retry">Försök med att Tvinga appen att stänga och återstarta. En felaktig anslutning till Nextcloud-appen kan ha förekommit.</string> + <string name="error_dialog_tip_clear_storage_might_help">Om problemet kvarstår kan du försöka rensa lagringen av båda apparna: Nextcloud och Nextcloud Deck för att lösa problemet.</string> + <string name="error_dialog_tip_database_upgrade_failed">Uppgraderingen av databasen misslyckades. Var god anmäl problemet, rensa sedan appens data för att kunna återgå till normal användning.</string> + <string name="error_dialog_tip_clear_storage">Du kan rensa diskutrymmet genom att öppna appens info, välj Lagring -> Rensa data</string> + <string name="error_dialog_tip_files_outdated">Din Nextcloud-app verkar utdaterad. Besök Play Butiken eller F-Droid för att hämta senaste versionen</string> + <string name="error_dialog_tip_files_force_stop">Något verkar fel med din Nextcloud-app. Försök att tvångsavsluta både Nextcloud och Nextcloud Deck-appen.</string> + <string name="error_dialog_tip_files_delete_storage">Om tvångsavslutning ej hjälper kan du försöka rensa datan på båda appar.</string> + <string name="error_dialog_timeout_instance">Din server svarade inte inom den angivna tiden. Kontrollera att den fungerar normalt.</string> + <string name="error_dialog_timeout_toggle">Kontrollera din nätverksanslutning. Ibland kan det hjälpa att slå av och på mobildata eller Wi-Fi.</string> + <string name="error_dialog_check_server">Inkorrekt svar från din server. Kontrollera att du har tillgång till Deck-appen genom din webbläsare.</string> + <string name="error_dialog_check_server_logs">Det finns problem med din Nextcloud-installation. Ta gärna en titt i serverns loggfiler. </string> + <string name="error_dialog_check_maintenance">Kontrollera att underhållsläge inte är aktivt på Nextcloud-installation.</string> + <string name="error_dialog_insufficient_storage">Din Nextcloud-installation har slut på diskutrymme. Radera några filer för att kunna synkronisera ändringar till ditt moln.</string> + <string name="error_dialog_we_need_info">Vi behöver följande teknisk information för att kunna hjälpa dig:</string> + <string name="error_dialog_redirect">Din server svarade med ett HTTP 302 statusmeddelande vilket innebär att du inte har installerat Deck appen på din server eller att något är felkonfigurerat. Detta kan orsakas av manuella ändringar i en .htaccess-fil eller av Nextcloud-appar som \"OID Client\".</string> + <string name="error_dialog_version_not_parsable">Vi kunde inte avgöra vilken version av Deck-appen som är installerad på servern. Kontrollera att den är installerad och aktiverad.</string> + <string name="error_dialog_account_might_not_be_authorized">Ditt Nextcloud-konto är eventuellt inte längre auktoriserat. </string> <string name="error_action_open_deck_info">Öppna appinformation</string> <string name="error_action_open_network">Nätverksinställningar</string> <string name="error_action_server_logs">Serverloggar</string> @@ -223,6 +239,7 @@ <string name="info_box_version_not_supported">Serverversion %1$s stöds inte, uppdatera till %2$s</string> <string name="share_link">Dela länk</string> <string name="archive_cards">Arkivera kort</string> + <string name="manage_accounts">Hantera konton</string> <string name="manage_list">Hantera lista</string> <string name="simple_reply">Svara</string> @@ -230,9 +247,11 @@ <string name="append_text_to_description">Lägg till beskrivning</string> <string name="add_text_as_comment">Lägg till som kommentar</string> <string name="progress_count">%1$d av %2$d</string> - <plurals name="progress_error_count"> - <item quantity="one">%1$d fel vid uppladdning.</item> - <item quantity="other">%1$d fel vid uppladdning</item> - </plurals> <string name="simple_report">Rapportera</string> - </resources> + <string name="clone_board">Kopiera tavla</string> + <string name="widget_stack_title">Lista</string> + <string name="project_type_file">Fil</string> + <string name="projects_title">Projekt</string> + <string name="simple_move">Flytta</string> + <string name="simple_unassign">Ta bort tilldelning</string> +</resources> diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index a34bb2f8a..194520815 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -38,6 +38,7 @@ <string name="simple_disabled">devre dışı</string> <string name="simple_copied">Kopyalandı</string> <string name="simple_archive">Arşivle</string> + <string name="simple_unassigned">Atanmamış</string> <string name="edit_board">Panoyu düzenle</string> <string name="archive_board">Panoyu arşivle</string> @@ -70,9 +71,6 @@ <string name="about_credits_tab_title">Emeği Geçenler</string> <string name="about_contribution_tab_title">Katkıda Bulunanlar</string> <string name="about_license_tab_title">Lisans</string> - - <string name="copied_to_clipboard">Panoya kopyalandı</string> - <string name="seconds_ago">saniye önce</string> <string name="edit">Düzenle</string> <string name="label_labels">Etiketleri Seçin</string> @@ -113,6 +111,7 @@ <string name="no_lists_yet">Henüz bir liste yok</string> <string name="do_you_want_to_save_your_changes">Değişiklikleri kaydetmek ister misiniz?</string> <string name="do_you_want_to_archive_all_cards_of_the_list">Tüm %1$s kartlarını arşivlemek ister misiniz?</string> + <string name="do_you_want_to_archive_all_cards_of_the_filtered_list">Tüm %1$s süzülmüş kartlarını arşivlemek ister misiniz?</string> <plurals name="do_you_want_to_delete_the_current_list"> <item quantity="one">Bu işlem, bu listedeki tüm %1$d kartı kalıcı olarak silecek.</item> <item quantity="other">Bu işlem bu listedeki tüm %1$d kartı kalıcı olarak silecek.</item> @@ -129,6 +128,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> @@ -156,7 +156,6 @@ <string name="maintenance_mode_explanation">%1$s sunucusu şu anda bakım kipinde. Lütfen yöneticinizle görüşün ya da daha sonra yeniden deneyin.</string> <string name="share_add_to_card">Karta ekle</string> <string name="share_success">%1$s, %2$s üzerine eklendi</string> - <string name="could_not_copy_to_clipboard">Panoya kopyalanamadı</string> <string name="add_comment">Yorum ekle</string> <string name="card_edit_comments">Yorumlar</string> <string name="no_comments_yet">Henüz bir yorum yok</string> @@ -216,7 +215,7 @@ <string name="error_dialog_title">Hayıır. Şimdi ne olacak? 🙁</string> <string name="error_dialog_tip_token_mismatch_retry">Lütfen uygulamayı kapanmaya zorlayıp yeniden başlatın. Yanlış bir Nextcloud uygulaması bağlantısı olmalı.</string> - <string name="error_dialog_tip_token_mismatch_clear_storage">Sorun sürerse, çözmek için hem Nextcloud uygulamasının hem de Nextcloud Tahta uygulamasının depolamalarını temizlemeyi deneyin.</string> + <string name="error_dialog_tip_clear_storage_might_help">Sorun sürerse, çözmek için hem Nextcloud uygulamasının hem de Nextcloud Tahta uygulamasının depolamalarını temizlemeyi deneyin.</string> <string name="error_dialog_tip_database_upgrade_failed">Veritabanı güncellenemedi. Lütfen sorunu bildirin ve uygulamayı normal şekilde kullanabilmek için depolamayı temizleyin.</string> <string name="error_dialog_tip_clear_storage">Depolamayı temizlemek için uygulama bilgilerini açın ve Depolama (Hafıza) → Depolamayı temizle (Veriyi sil) seçin.</string> <string name="error_dialog_tip_files_outdated">Nextcloud uygulamanız eski görünüyor. Lütfen son sürümü almak için Play Store ya da F-Droid mağazalarına gidin.</string> @@ -231,6 +230,8 @@ <string name="error_dialog_we_need_info">Size yardımcı olabilmemiz için şu teknik bilgilere gerek duyacağız:</string> <string name="error_dialog_redirect">Sunucunuz, üzerine Tahta uygulamasının kurulmadığı ya da bir şeyin yanlış yapılandırıldığı anlamına gelen HTTP 302 durum kodu ile yanıt verdi. Bu durum, bir .htaccess dosyasındaki özel kısıtlamalardan ya da OID İstemcisi gibi Nextcloud uygulamalarından kaynaklanabilir.</string> <string name="error_dialog_version_not_parsable">Sunucu tarafındaki tahta uygulamasının sürümü belirlenemedi. Lütfen kurulmuş ve etkinleştirilmiş olduğundan emin olun.</string> + <string name="error_dialog_account_might_not_be_authorized">Nextcloud uygulamanızın hesabına artık izin verilmiyor olabilir.</string> + <string name="error_dialog_user_not_found_in_database">Geçerli kullanıcı veritabanımızdaki kullanıcı ile eşleşmiyor. Nextcloud kopyanızda LDAP kullanıyorsanız, Nextcloud uygulamanızda eski bir kullanıcı kodu kayıtlı olabilir.</string> <string name="error_dialog_capabilities_not_parsable">Sunucunuzun özellikleri alınamadı. Lütfen sunucunuzun düzgün çalışır durumda olduğundan ve diğer istemci uygulamalarının Nextcloud sunucusuna erişebildiğinden emin olun.</string> <string name="error_dialog_attachment_upload_failed">Bir ek dosya yüklenemedi. Lütfen dosyayı başka bir şekilde paylaşın ve bu hatayı bize bildirin.</string> <string name="error_dialog_tip_disable_battery_optimizations">Lütfen Nextcloud ve Pano uygulaması için tüm pil iyileştirmelerini devre dışı bırakın.</string> @@ -243,6 +244,7 @@ <string name="info_box_version_not_supported">%1$s sunucu sürümü desteklenmiyor. Lütfen %2$s sürümüne güncelleyin</string> <string name="share_link">Bağlantıyı paylaş</string> <string name="archive_cards">Kartları arşivle</string> + <string name="manage_accounts">Hesap yönetimi</string> <string name="manage_list">Liste yönetimi</string> <string name="simple_reply">Yanıtla</string> @@ -251,9 +253,37 @@ <string name="add_text_as_comment">Yorum olarak ekle</string> <string name="progress_count">%1$d / %2$d</string> <plurals name="progress_error_count"> - <item quantity="one">Yükleme sırasında %1$d sorun çıktı.</item> - <item quantity="other">Yükleme sırasında %1$d sorun çıktı.</item> + <item quantity="one">Yükleme sırasında %1$d sorun çıktı</item> + <item quantity="other">Yükleme sırasında %1$d sorun çıktı</item> </plurals> <string name="simple_report">Hata bildirin</string> <string name="error_action_open_battery_settings">Pil ayarları</string> + <string name="move_warning">Kart başka bir panoya taşınırken yorum ya da ek dosyalar aktarılamaz.</string> + <string name="clone_board">Panoyu kopyala</string> + <string name="cloning_board">%1$s kopyalanıyor…</string> + <string name="successfully_cloned_board">%1$s kopyalandı</string> + <string name="attachment_does_not_yet_exist">Ek dosya henüz panoda bulunmuyor</string> + <string name="card_does_not_yet_exist">Kart henüz panoda bulunmuyor</string> + + <string name="widget_stack_title">Listele</string> + <string name="widget_stack_header_icon">Araç başlık simgesi</string> + <string name="widget_stack_placeholder_icon">Araç yer belirteci simgesi</string> + <string name="select_stack">Liste seçin</string> + <string name="project_type_deck_board">Tahta panosu</string> + <string name="project_type_deck_card">Tahta kartı</string> + <string name="project_type_file">Dosya</string> + <string name="projects_title">Projeler</string> + <plurals name="resources_count"> + <item quantity="one">%1$d kaynak</item> + <item quantity="other">%1$d kaynak</item> + </plurals> + <string name="no_assigned_label">Henüz bir etiket atanmamış</string> + <string name="single_card">Tek kart</string> + <string name="project_type_room">Talk odası</string> + <string name="simple_move">Taşı</string> + <string name="cannot_upload_files_without_permission">İzin olmadan dosya yüklenemez</string> + <string name="clone_cards">Kartları kopyala</string> + <string name="simple_clone">Kopyala</string> + <string name="user_avatar">Kullanıcı avatarı</string> + <string name="simple_unassign">Atamayı kaldır</string> </resources> diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 24f1a7d0f..f82c949dc 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -38,6 +38,7 @@ <string name="simple_disabled">вимкнено</string> <string name="simple_copied">Скопійовано</string> <string name="simple_archive">Архів</string> + <string name="simple_unassigned">Скасовано призначення</string> <string name="edit_board">Редагувати дошку</string> <string name="archive_board">Архівувати дошку</string> @@ -70,9 +71,6 @@ <string name="about_credits_tab_title">Подяки</string> <string name="about_contribution_tab_title">Внесок</string> <string name="about_license_tab_title">Ліцензія</string> - - <string name="copied_to_clipboard">Скопійовано</string> - <string name="seconds_ago">секунд тому</string> <string name="edit">Редагувати</string> <string name="label_labels">Вибрати позначки</string> @@ -112,6 +110,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> @@ -159,7 +158,6 @@ <string name="maintenance_mode_explanation">Зараз сервер %1$s у режимі технічного обслуговування. Будь ласка, сконтактуйте з вашим адміністратором або спробуйте пізніше.</string> <string name="share_add_to_card">Додати до картки</string> <string name="share_success">Успішно додано %1$s до %2$s</string> - <string name="could_not_copy_to_clipboard">Неможливо скопіювати</string> <string name="add_comment">Додати коментар</string> <string name="card_edit_comments">Коментарі</string> <string name="no_comments_yet">Коментарі відсутні</string> @@ -215,9 +213,18 @@ <string name="server_misconfigured">Сервер налаштовано некоректно</string> <string name="server_error">Помилка серверу</string> <string name="shared_error">Спільний доступ до вмісту з інших застосунків ще не підтримується повністю. Будь ласка, спробуйте спочатку звантажити цей вміст, а потім поділитися ним через вбудований менеджер файлів або галерею.</string> + <string name="error_edit_activity_killed_by_android">Android вивантажив режим редагування через нестачу системних ресурсів.</string> + <string name="error_dialog_title">От халепа!</string> <string name="error_dialog_tip_token_mismatch_retry">Спробуйте примусово закрити застосунок та наново запустити. Можливо, встановлено неправильне з\'єднання із застосунком Nextcloud.</string> + <string name="error_dialog_tip_clear_storage">Ви можете очистити сховище, відкривши інформацію про програму та вибравши Пам’ять → Очистити пам’ять.</string> + <string name="error_dialog_tip_files_outdated">Ваш додаток Nextcloud здається застарілим. Будь ласка, відвідайте Play Маркет або F-Droid, щоб отримати останню версію.</string> + <string name="error_dialog_tip_files_delete_storage">Якщо примусове їх завершення не допомагає, спробуйте очистити сховище обох програм.</string> + <string name="error_dialog_timeout_instance">За відведений час відповіді від вашого сервера не надійшло. Переконайтесь, що ваш сервер працює добре.</string> + <string name="error_dialog_check_server_logs">Виникла проблема з налаштуванням Nextcloud. Будь ласка, подивіться файли журналу сервера.</string> + <string name="error_dialog_check_maintenance">Будь ласка, перевірте, чи ваш сервер Nextcloud наразі не перебуває в режимі обслуговування.</string> <string name="error_dialog_insufficient_storage">Ваш сервер Nextcloud не має вільного місця. Будь ласка видаліть деякі файлі, щоб синхронізувати ваші локальні зміни з вашим хмарним сховищем.</string> + <string name="error_dialog_we_need_info">Нам потрібна наступна технічна інформація, щоб допомогти вам:</string> <string name="error_action_open_deck_info">Відкрити інформацію про застосунок</string> <string name="error_action_open_network">Налаштування мережі</string> <string name="error_action_server_logs">Журнал сервера</string> @@ -226,7 +233,29 @@ <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> <string name="simple_report">Звіт</string> + <string name="error_action_open_battery_settings">Налаштування батареї</string> + <string name="move_warning">У разі переміщення картки до іншої дошки коментарі та вкладення не буде перенесено.</string> + <string name="clone_board">Копіювати дошку</string> + <string name="cloning_board">Копіювання %1$s...</string> + <string name="successfully_cloned_board">Успішно скопійовано %1$s</string> + <string name="attachment_does_not_yet_exist">Вкладення поки відсутнє у застосунку</string> + <string name="card_does_not_yet_exist">Картка поки відсутня у застосунку</string> + + <string name="widget_stack_title">Список</string> + <string name="widget_stack_header_icon">Піктограма заголовку віджету</string> + <string name="widget_stack_placeholder_icon">Піктограма поля віджету</string> + <string name="select_stack">Виберіть список</string> + <string name="project_type_file">Файл</string> + <string name="projects_title">Проєкти</string> + <string name="simple_move">Перемістити</string> </resources> diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index cdfb70ae2..428e77ed4 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -38,6 +38,7 @@ <string name="simple_disabled">已禁用</string> <string name="simple_copied">已复制</string> <string name="simple_archive">归档</string> + <string name="simple_unassigned">未分配</string> <string name="edit_board">编辑面板</string> <string name="archive_board">归档面板</string> @@ -70,9 +71,6 @@ <string name="about_credits_tab_title">致谢</string> <string name="about_contribution_tab_title">贡献</string> <string name="about_license_tab_title">授权</string> - - <string name="copied_to_clipboard">复制到剪贴板</string> - <string name="seconds_ago">几秒前</string> <string name="edit">编辑</string> <string name="label_labels">选择标签</string> @@ -153,7 +151,6 @@ <string name="maintenance_mode_explanation">服务器 %1$s 正处于维护模式。请联系你的管理员或稍后再试。</string> <string name="share_add_to_card">添加到卡片</string> <string name="share_success">成功将 %1$s 添加到 %2$s</string> - <string name="could_not_copy_to_clipboard">无法复制到剪贴板</string> <string name="add_comment">添加评论</string> <string name="card_edit_comments">评论</string> <string name="no_comments_yet">还没有评论</string> @@ -209,7 +206,7 @@ <string name="shared_error">目前还不完全支持从第三方应用程序中分享内容。请尝试先下载,然后从您的本地文件管理器或图库中分享。</string> <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_clear_storage_might_help">如果问题持续存在,尝试通过清除 Nextcloud 和 Nextcloud 看板两个应用的存储空间来解决问题。</string> <string name="error_dialog_tip_files_outdated">您的 Nextcloud 应用似乎已经过时了。请访问 Play 商店或 F-Droid 以获取最新版本。</string> <string name="error_dialog_tip_files_force_stop">您的 Nextcloud 应用似乎出现了问题。请尝试强行停止 Nextcloud 和 Nextcloud 看板两个应用。</string> <string name="error_dialog_tip_files_delete_storage">如果强行停止没有效果,您可以尝试清除两个应用的存储空间。</string> @@ -225,4 +222,10 @@ <string name="manage_accounts">管理账号</string> <string name="simple_reply">回复</string> <string name="simple_report">报告</string> - </resources> + <string name="clone_board">克隆面板</string> + <string name="widget_stack_title">列表</string> + <string name="project_type_file">文件</string> + <string name="projects_title">项目</string> + <string name="simple_move">移动</string> + <string name="simple_unassign">取消分配</string> +</resources> diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 6db0c7441..55812677c 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -3,17 +3,13 @@ <color name="primary">@android:color/white</color> <color name="accent">#000000</color> <color name="defaultBrand">#0082C9</color> - <color name="toolbarEditTextHighlightColor">#55ffffff</color> <color name="danger">#d40000</color> - <color name="fg_accent">#fff</color> - <color name="fg_primary">#333</color> <color name="fg_secondary">#999</color> <color name="bg_highlighted">#eee</color> <color name="grey600">#757575</color> <color name="bg_info_box">#dddddd</color> <color name="bg_card">@android:color/white</color> <color name="bg_card_wrapper">#fafafa</color> - <color name="dark_fg_primary">#e5e5e5</color> diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index db8b51610..8aa82c1cf 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -1,8 +1,12 @@ <resources> + <dimen name="spacer_1qx">2dp</dimen> <dimen name="spacer_1hx">4dp</dimen> <dimen name="spacer_1x">8dp</dimen> <dimen name="spacer_2x">16dp</dimen> <dimen name="spacer_3x">24dp</dimen> + <dimen name="spacer_4x">32dp</dimen> + + <dimen name="compact_label_height">6dp</dimen> <!-- Drawer header --> <dimen name="drawer_header_height">100dp</dimen> @@ -16,11 +20,9 @@ <dimen name="avatar_size_small_overlapping_border">2dp</dimen> <dimen name="icon_size_details">24dp</dimen> - <dimen name="nav_drawer_header_avatar">56dp</dimen> - <dimen name="nav_drawer_header_avatar_other_accounts_size">40dp</dimen> - <dimen name="nav_drawer_header_avatar_second_account_margin">56dp</dimen> - <dimen name="drawer_header_text">14sp</dimen> - <dimen name="drawer_header_subtext">12sp</dimen> - <dimen name="text_size_attachments">14sp</dimen> <dimen name="empty_content_font_size">26sp</dimen> + + <dimen name="widget_stack_card_padding">@dimen/spacer_1hx</dimen> + <dimen name="widget_stack_icon_width">30dp</dimen> + <dimen name="widget_stack_header_height">36dp</dimen> </resources>
\ No newline at end of file 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 db0dc6400..2d3b16cda 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -40,6 +40,7 @@ <string name="simple_disabled">disabled</string> <string name="simple_copied">Copied</string> <string name="simple_archive">Archive</string> + <string name="simple_unassigned">Unassigned</string> <string name="edit_board">Edit board</string> <string name="archive_board">Archive board</string> @@ -76,9 +77,6 @@ <string name="about_credits_tab_title">Credits</string> <string name="about_contribution_tab_title">Contribution</string> <string name="about_license_tab_title">License</string> - - <string name="copied_to_clipboard">Copied to clipboard</string> - <string name="seconds_ago">seconds ago</string> <string name="edit">Edit</string> <string name="label_labels">Select Tags</string> @@ -138,6 +136,7 @@ <string name="no_lists_yet">No lists yet</string> <string name="do_you_want_to_save_your_changes">Do you want to save your changes?</string> <string name="do_you_want_to_archive_all_cards_of_the_list">Do you want to archive all cards of %1$s?</string> + <string name="do_you_want_to_archive_all_cards_of_the_filtered_list">Do you want to archive all filtered cards of %1$s?</string> <plurals name="do_you_want_to_delete_the_current_list"> <item quantity="one">This will permanently delete %1$d card of this list.</item> <item quantity="other">This will permanently delete all %1$d cards of this list.</item> @@ -154,6 +153,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> @@ -181,7 +181,6 @@ <string name="maintenance_mode_explanation">The server %1$s is currently in maintenance mode. Please contact your administrator or try later again.</string> <string name="share_add_to_card">Add to card</string> <string name="share_success">Successfully added %1$s to %2$s</string> - <string name="could_not_copy_to_clipboard">Could not copy to clipboard</string> <string name="add_comment">Add comment</string> <string name="card_edit_comments">Comments</string> <string name="no_comments_yet">No comments yet</string> @@ -241,8 +240,8 @@ <string name="error_dialog_title">Oh no - What now? 🙁</string> <string name="error_dialog_tip_token_mismatch_retry">Please try to force close the app and restart it again. There might have been an incorrect connection to the Nextcloud app.</string> - <string name="error_dialog_tip_token_mismatch_clear_storage">If the issue persists, try to clear the storage of both apps: Nextcloud and Nextcloud Deck to solve this issue.</string> - <string name="error_dialog_tip_database_upgrade_failed">The ugprade of the database failed. Please report the issue and clear the storage to use the app normally.</string> + <string name="error_dialog_tip_clear_storage_might_help">If the issue persists, try to clear the storage of both apps: Nextcloud and Nextcloud Deck to solve this issue.</string> + <string name="error_dialog_tip_database_upgrade_failed">The upgrade of the database failed. Please report the issue and clear the storage to use the app normally.</string> <string name="error_dialog_tip_clear_storage">You can clear the storage by opening the app info and selecting Storage → Clear storage.</string> <string name="error_dialog_tip_files_outdated">Your Nextcloud app seems to be outdated. Please visit the Play Store or F-Droid to get the latest version.</string> <string name="error_dialog_tip_files_force_stop">Something seems to be wrong with your Nextcloud app. Please try to force stop both, the Nextcloud app and the Nextcloud Deck app.</string> @@ -256,6 +255,8 @@ <string name="error_dialog_we_need_info">We need the following technical information to help you:</string> <string name="error_dialog_redirect">Your server did respond with a HTTP 302 status code, which implies, that you do not have installed the Deck app on your server or something is misconfigured. This can be caused by custom overrides in a .htaccess-file or by Nextcloud apps like OID Client.</string> <string name="error_dialog_version_not_parsable">We could not determine the version of the server side Deck app. Please make sure it is installed and enabled.</string> + <string name="error_dialog_account_might_not_be_authorized">The account of your Nextcloud app might no longer be authorized.</string> + <string name="error_dialog_user_not_found_in_database">The current user does not match the user we have in our database. If you are using LDAP on your Nextcloud instance, your Nextcloud app might have stored an old user ID.</string> <string name="error_dialog_capabilities_not_parsable">We could not fetch the capabilities of your server. Please make sure your server is running well and other client apps are able to access Nextcloud.</string> <string name="error_dialog_attachment_upload_failed">An attachment could not be uploaded. Please try to share it on another way and let us know about this bug.</string> <string name="error_dialog_tip_disable_battery_optimizations">Please disable all battery optimizations for Nextcloud and the Deck app.</string> @@ -268,6 +269,7 @@ <string name="info_box_version_not_supported">Server version %1$s not supported, please update to %2$s</string> <string name="share_link">Share link</string> <string name="archive_cards">Archive cards</string> + <string name="manage_accounts">Manage accounts</string> <string name="manage_list">Manage list</string> <string name="simple_reply">Reply</string> @@ -276,11 +278,39 @@ <string name="add_text_as_comment">Add as comment</string> <string name="progress_count">%1$d of %2$d</string> <plurals name="progress_error_count"> - <item quantity="one">%1$d error while uploading.</item> + <item quantity="one">%1$d error while uploading</item> <item quantity="other">%1$d errors while uploading</item> </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="attachment_does_not_yet_exist">Attachment does not yet exist in Deck</string> + <string name="card_does_not_yet_exist">Card does not yet exist in Deck</string> + + <string name="widget_stack_title">List</string> + <string name="widget_stack_header_icon">Widget header icon</string> + <string name="widget_stack_placeholder_icon">Widget placeholder icon</string> + <string name="select_stack">Select list</string> + <string name="project_type_deck_board">Deck board</string> + <string name="project_type_deck_card">Deck card</string> + <string name="project_type_file">File</string> + <string name="projects_title">Projects</string> + <plurals name="resources_count"> + <item quantity="one">%1$d resource</item> + <item quantity="other">%1$d resources</item> + </plurals> + <string name="no_assigned_label">No assigned tag</string> + <string name="single_card">Single card</string> + <string name="project_type_room">Talk room</string> + <string name="simple_move">Move</string> + <string name="cannot_upload_files_without_permission">Can not upload files without permission</string> + <string name="clone_cards">Clone cards</string> + <string name="simple_clone">Clone</string> + <string name="user_avatar">User avatar</string> + <string name="simple_unassign">Unassign</string> <string name="simple_contact">Contact</string> <string name="simple_file">File</string> <string name="simple_camera">Camera</string> diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index f0bff78fe..c253ae461 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -6,6 +6,7 @@ <item name="colorAccent">@color/accent</item> <item name="toolbarStyle">@style/toolbarStyle</item> <item name="android:windowBackground">?attr/colorPrimary</item> + <item name="textAppearanceHeadline1">@style/Deck.TextAppearance.Headline1</item> </style> <style name="toolbarStyle" parent="@style/Widget.AppCompat.Toolbar"> @@ -32,4 +33,8 @@ <item name="android:colorBackgroundCacheHint">@null</item> <item name="android:windowIsTranslucent">true</item> </style> + + <style name="Deck.TextAppearance.Headline1" parent="TextAppearance.MaterialComponents.Headline1"> + <item name="android:textSize">36sp</item> + </style> </resources> diff --git a/app/src/main/res/xml/settings.xml b/app/src/main/res/xml/settings.xml index 14943fff6..6d67622bd 100644 --- a/app/src/main/res/xml/settings.xml +++ b/app/src/main/res/xml/settings.xml @@ -29,6 +29,12 @@ android:icon="@drawable/ic_format_paint_grey600_24dp" android:key="@string/pref_key_branding" android:title="@string/settings_branding_title" - app:defaultValue="true" /> + app:defaultValue="false" /> + + <it.niedermann.nextcloud.deck.ui.branding.BrandedSwitchPreference + android:icon="@drawable/ic_baseline_compact_24" + android:key="@string/pref_key_compact" + android:title="@string/settings_compact_title" + app:defaultValue="false" /> </it.niedermann.nextcloud.deck.ui.branding.BrandedPreferenceCategory> </PreferenceScreen> diff --git a/app/src/main/res/xml/stack_widget_provider.xml b/app/src/main/res/xml/stack_widget_provider.xml new file mode 100644 index 000000000..3712625da --- /dev/null +++ b/app/src/main/res/xml/stack_widget_provider.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" + android:initialLayout="@layout/widget_stack" + android:minHeight="110dp" + android:minWidth="180dp" + android:minResizeHeight="40dp" + android:minResizeWidth="80dp" + android:resizeMode="vertical|horizontal" + android:updatePeriodMillis="86400000" + android:widgetCategory="keyguard|home_screen" + android:previewImage="@drawable/widget_stack_preview" + android:configure="it.niedermann.nextcloud.deck.ui.widget.stack.StackWidgetConfigurationActivity" /> diff --git a/app/src/main/widget_stack_preview-playstore.png b/app/src/main/widget_stack_preview-playstore.png Binary files differnew file mode 100644 index 000000000..fdd955b2c --- /dev/null +++ b/app/src/main/widget_stack_preview-playstore.png diff --git a/app/src/test/java/it/niedermann/nextcloud/deck/CommentsUtilTest.java b/app/src/test/java/it/niedermann/nextcloud/deck/CommentsUtilTest.java new file mode 100644 index 000000000..47a23392e --- /dev/null +++ b/app/src/test/java/it/niedermann/nextcloud/deck/CommentsUtilTest.java @@ -0,0 +1,41 @@ +package it.niedermann.nextcloud.deck; + +import org.junit.Assert; +import org.junit.Test; + +import it.niedermann.nextcloud.deck.ui.card.comments.util.CommentsUtil; + +public class CommentsUtilTest { + + @Test + public void testMentionDiscovery() { + Assert.assertNull(CommentsUtil.getUserNameForMentionProposal("", 0)); + Assert.assertNull(CommentsUtil.getUserNameForMentionProposal("a ", 1)); + Assert.assertNull(CommentsUtil.getUserNameForMentionProposal("a ", 2)); + Assert.assertNull(CommentsUtil.getUserNameForMentionProposal("@a ", 3)); + Assert.assertNull(CommentsUtil.getUserNameForMentionProposal("d@a", 2)); + Assert.assertNull(CommentsUtil.getUserNameForMentionProposal("d@a", 0)); + Assert.assertNull(CommentsUtil.getUserNameForMentionProposal("d@a", 3)); + Assert.assertNull(CommentsUtil.getUserNameForMentionProposal("ab", 0)); + Assert.assertNull(CommentsUtil.getUserNameForMentionProposal("ab", 1)); + Assert.assertNull(CommentsUtil.getUserNameForMentionProposal("ab", 2)); + Assert.assertNull(CommentsUtil.getUserNameForMentionProposal("def @ab ", 8)); + Assert.assertEquals("ab", CommentsUtil.getUserNameForMentionProposal("\n@ab", 3).first); + Assert.assertEquals("ab", CommentsUtil.getUserNameForMentionProposal("\n@ab", 4).first); + Assert.assertEquals("ab", CommentsUtil.getUserNameForMentionProposal("\t@ab", 3).first); + Assert.assertEquals("ab", CommentsUtil.getUserNameForMentionProposal(" @ab", 3).first); + Assert.assertEquals("ab", CommentsUtil.getUserNameForMentionProposal("@ab", 3).first); + Assert.assertEquals("ab", CommentsUtil.getUserNameForMentionProposal("@ab", 2).first); + Assert.assertEquals("ab", CommentsUtil.getUserNameForMentionProposal("@ab def", 2).first); + Assert.assertEquals("ab", CommentsUtil.getUserNameForMentionProposal("@ab def", 3).first); + Assert.assertEquals("ab", CommentsUtil.getUserNameForMentionProposal("def @ab ", 5).first); + Assert.assertEquals("ab", CommentsUtil.getUserNameForMentionProposal("def @ab", 6).first); + Assert.assertEquals("ab_asdf_ldklsdkf", CommentsUtil.getUserNameForMentionProposal("def @ab_asdf_ldklsdkf", 7).first); + Assert.assertEquals("ab_asdf_ldklsdkf", CommentsUtil.getUserNameForMentionProposal("def @ab_asdf_ldklsdkf ", 7).first); + Assert.assertEquals("\"ab_asdf_ldklsdkf\"", CommentsUtil.getUserNameForMentionProposal("def @\"ab_asdf_ldklsdkf\"", 7).first); + Assert.assertEquals("\"ab_asdf_ldklsdkf\"", CommentsUtil.getUserNameForMentionProposal("def @\"ab_asdf_ldklsdkf\" \nasdf", 7).first); + Assert.assertEquals("ab", CommentsUtil.getUserNameForMentionProposal("def @ab\n", 7).first); + Assert.assertEquals("ab", CommentsUtil.getUserNameForMentionProposal("\ndef @ab\n", 8).first); + Assert.assertEquals("ab", CommentsUtil.getUserNameForMentionProposal("\n def @ab\nasdfasdf", 9).first); + } +} diff --git a/app/src/test/java/it/niedermann/nextcloud/deck/ProjectUtilTest.java b/app/src/test/java/it/niedermann/nextcloud/deck/ProjectUtilTest.java new file mode 100644 index 000000000..2c03f5952 --- /dev/null +++ b/app/src/test/java/it/niedermann/nextcloud/deck/ProjectUtilTest.java @@ -0,0 +1,58 @@ +package it.niedermann.nextcloud.deck; + +import org.junit.Test; + +import it.niedermann.nextcloud.deck.util.ProjectUtil; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertThrows; + +public class ProjectUtilTest { + @Test + public void extractBoardIdAndCardIdFromUrl() { + // Valid board URLs + assertArrayEquals(ProjectUtil.extractBoardIdAndCardIdFromUrl("index.php/apps/deck/#/board/4"), new long[]{4}); + assertArrayEquals(ProjectUtil.extractBoardIdAndCardIdFromUrl("/index.php/apps/deck/#/board/4"), new long[]{4}); + assertArrayEquals(ProjectUtil.extractBoardIdAndCardIdFromUrl("example.com/index.php/apps/deck/#/board/4"), new long[]{4}); + assertArrayEquals(ProjectUtil.extractBoardIdAndCardIdFromUrl("http://example.com/index.php/apps/deck/#/board/4"), new long[]{4}); + assertArrayEquals(ProjectUtil.extractBoardIdAndCardIdFromUrl("https://example.com/index.php/apps/deck/#/board/4"), new long[]{4}); + assertArrayEquals(ProjectUtil.extractBoardIdAndCardIdFromUrl("https://example.com/nextcloud/index.php/apps/deck/#/board/4"), new long[]{4}); + assertArrayEquals(ProjectUtil.extractBoardIdAndCardIdFromUrl("https://example.com/nextcloud/index.php/apps/deck/#/board/4/card"), new long[]{4}); + assertArrayEquals(ProjectUtil.extractBoardIdAndCardIdFromUrl("https://example.com/nextcloud/index.php/apps/deck/#/board/4/card/"), new long[]{4}); + assertArrayEquals(ProjectUtil.extractBoardIdAndCardIdFromUrl("https://example.com/nextcloud/index.php/apps/deck/#/board/4/card/0"), new long[]{4}); + assertArrayEquals(ProjectUtil.extractBoardIdAndCardIdFromUrl("https://example.com/nextcloud/index.php/apps/deck/#/board/4/foo"), new long[]{4}); + + // Valid card URLs + assertArrayEquals(ProjectUtil.extractBoardIdAndCardIdFromUrl("index.php/apps/deck/#/board/4/card/6"), new long[]{4, 6}); + assertArrayEquals(ProjectUtil.extractBoardIdAndCardIdFromUrl("/index.php/apps/deck/#/board/4/card/6"), new long[]{4, 6}); + assertArrayEquals(ProjectUtil.extractBoardIdAndCardIdFromUrl("example.com/index.php/apps/deck/#/board/4/card/6"), new long[]{4, 6}); + assertArrayEquals(ProjectUtil.extractBoardIdAndCardIdFromUrl("http://example.com/index.php/apps/deck/#/board/4/card/6"), new long[]{4, 6}); + assertArrayEquals(ProjectUtil.extractBoardIdAndCardIdFromUrl("https://example.com/index.php/apps/deck/#/board/4/card/6"), new long[]{4, 6}); + assertArrayEquals(ProjectUtil.extractBoardIdAndCardIdFromUrl("https://example.com/index.php/apps/deck/#/board/4/card/6/"), new long[]{4, 6}); + assertArrayEquals(ProjectUtil.extractBoardIdAndCardIdFromUrl("https://example.com/nextcloud/index.php/apps/deck/#/board/4/card/6"), new long[]{4, 6}); + + // URLs to talk + assertThrows(IllegalArgumentException.class, () -> ProjectUtil.extractBoardIdAndCardIdFromUrl("index.php/call/qkzhe5k2")); + assertThrows(IllegalArgumentException.class, () -> ProjectUtil.extractBoardIdAndCardIdFromUrl("/index.php/call/qkzhe5k2")); + assertThrows(IllegalArgumentException.class, () -> ProjectUtil.extractBoardIdAndCardIdFromUrl("example.com/index.php/call/qkzhe5k2")); + assertThrows(IllegalArgumentException.class, () -> ProjectUtil.extractBoardIdAndCardIdFromUrl("http://example.com/index.php/call/qkzhe5k2")); + assertThrows(IllegalArgumentException.class, () -> ProjectUtil.extractBoardIdAndCardIdFromUrl("https://example.com/index.php/call/qkzhe5k2")); + assertThrows(IllegalArgumentException.class, () -> ProjectUtil.extractBoardIdAndCardIdFromUrl("https://example.com/nextcloud/index.php/call/qkzhe5k2")); + + // URLs to files + assertThrows(IllegalArgumentException.class, () -> ProjectUtil.extractBoardIdAndCardIdFromUrl("index.php/call/qkzhe5k2")); + assertThrows(IllegalArgumentException.class, () -> ProjectUtil.extractBoardIdAndCardIdFromUrl("/index.php/call/qkzhe5k2")); + assertThrows(IllegalArgumentException.class, () -> ProjectUtil.extractBoardIdAndCardIdFromUrl("example.com/index.php/call/qkzhe5k2")); + assertThrows(IllegalArgumentException.class, () -> ProjectUtil.extractBoardIdAndCardIdFromUrl("http://example.com/index.php/call/qkzhe5k2")); + assertThrows(IllegalArgumentException.class, () -> ProjectUtil.extractBoardIdAndCardIdFromUrl("https://example.com/index.php/call/qkzhe5k2")); + assertThrows(IllegalArgumentException.class, () -> ProjectUtil.extractBoardIdAndCardIdFromUrl("https://example.com/nextcloud/index.php/call/qkzhe5k2")); + + // Invalid URLs + //noinspection ConstantConditions + assertThrows(IllegalArgumentException.class, () -> ProjectUtil.extractBoardIdAndCardIdFromUrl(null)); + assertThrows(IllegalArgumentException.class, () -> ProjectUtil.extractBoardIdAndCardIdFromUrl("")); + assertThrows(IllegalArgumentException.class, () -> ProjectUtil.extractBoardIdAndCardIdFromUrl("/index.php/apps/deck/#/board/0")); + assertThrows(IllegalArgumentException.class, () -> ProjectUtil.extractBoardIdAndCardIdFromUrl("/index.php/apps/deck/#/board/0/card/3")); + assertThrows(IllegalArgumentException.class, () -> ProjectUtil.extractBoardIdAndCardIdFromUrl("/index.php/apps/deck/#/board//card/3")); + } +} |